diff --git a/src/components.d.ts b/src/components.d.ts index c3c224a..bcd7883 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -22,6 +22,8 @@ declare module 'vue' { ElCard: typeof import('element-plus/es')['ElCard'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup'] + ElCollapse: typeof import('element-plus/es')['ElCollapse'] + ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] ElDialog: typeof import('element-plus/es')['ElDialog'] ElDrawer: typeof import('element-plus/es')['ElDrawer'] diff --git a/src/modules/first-leg/components/FirstLegOrderEditorShell.vue b/src/modules/first-leg/components/FirstLegOrderEditorShell.vue index d790f7b..faa6a22 100644 --- a/src/modules/first-leg/components/FirstLegOrderEditorShell.vue +++ b/src/modules/first-leg/components/FirstLegOrderEditorShell.vue @@ -42,14 +42,24 @@ const emit = defineEmits<{ diff --git a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs index 4f17b40..17584b8 100644 --- a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs +++ b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs @@ -48,6 +48,13 @@ test('exposes real list, draft, submit, and detail workflow entries without canc assert.doesNotMatch(source, /cancelOrder|取消订单/) }) +test('returns edited submissions to the refreshed order list instead of opening detail', () => { + const submitSource = source.slice(source.indexOf('async function submitOrder()'), source.indexOf('async function searchOrders')) + assert.match(submitSource, /if \(isCreatePage\.value\)[\s\S]*router\.push\(\{ name: 'first-leg-orders' \}\)[\s\S]*return/) + assert.match(submitSource, /await loadPageData\(\)/) + assert.doesNotMatch(submitSource, /first-leg-order-detail/) +}) + test('copies an order into the dedicated create page without reusing order identity', () => { assert.match(source, /key: 'copy', label: '复制下单'/) assert.match(source, /name: 'first-leg-order-create'[\s\S]*copyFrom: order\.id/) @@ -351,7 +358,8 @@ test('shows a loading state before the list can create an order', () => { test('auto-fills trusted warehouse and company data instead of asking customers to repeat it', () => { assert.match(source, /async function selectDestinationWarehouse\(id: string\)/) assert.match(source, /await store\.loadWarehouseDetail\(warehouse\.id\)/) - assert.match(source, /availableWarehouses\.value\.length === 1/) + assert.match(source, /warehouse\.code\.toUpperCase\(\)\.includes\('YIWU'\)/) + assert.match(source, /warehouse\.name\.includes\('义乌'\)/) assert.match(source, /availableDestinationWarehouses\.value\.length === 1/) assert.match(source, /function createPickupPartySnapshot\(\)/) assert.match(source, /contactName: tenantStore\.company\.contactName/) @@ -415,6 +423,24 @@ test('distills order creation into the confirmed prototype sections with server assert.doesNotMatch(source, /v-model="form\.(?:businessType|origin|destination|priority|tradeTerm)"/) }) +test('matches the prototype two-column create layout and reset action', () => { + assert.match(source, /class="order-form-section prototype-section order-basic-section"/) + assert.match(source, /

客户下单<\/h3>

创建新订单<\/p>/) + assert.match(source, /\.is-create-route \.prototype-order-form \{[^}]*grid-template-columns: repeat\(2, minmax\(0, 1fr\)\)/) + assert.match(source, /\.is-create-route \.cargo-section, \.is-create-route \.cost-estimate-section \{ grid-column: 1 \/ -1; \}/) + assert.match(source, /\.is-create-route \.prototype-form-row :deep\(\.el-form-item__label\) \{[^}]*height: 40px[^}]*align-items: center[^}]*justify-content: flex-end/) + assert.match(source, /\.is-create-route \.compact-transport-grid > :deep\(\.el-form-item__label\) \{[^}]*align-items: center[^}]*text-align: right/) + assert.match(source, /async function resetCreateForm\(\)/) + assert.match(source, /确认清空当前填写的订单内容并恢复默认值/) + assert.match(source, /v-if="isCreatePage"[^>]*@click="resetCreateForm"[^>]*>]*\/>重置/) + assert.match(source, /class="submit-order-button"/) + assert.match(editorShellSource, /\.first-leg-editor-page \{[^}]*padding-bottom: 72px/) + assert.match(editorShellSource, /\.editor-page-footer \{[^}]*position: fixed[^}]*bottom: 0[^}]*left: 224px/) + assert.match(editorShellSource, /submit-gradient 5s ease-in-out infinite/) + assert.match(editorShellSource, /submit-shine 3\.2s ease-in-out infinite/) + assert.match(editorShellSource, /@media \(prefers-reduced-motion: reduce\)/) +}) + test('shows an optional LX inbound order number for one-click dropship orders', () => { assert.match(source, /v-if="form\.fulfillmentType === 'ONE_CLICK_DROPSHIP'" label="LX入库单号" prop="lxCode"/) assert.match(source, /v-model="form\.lxCode"[^>]*placeholder="请输入LX入库单号"/) @@ -478,6 +504,9 @@ test('shows field-level validation and concise guidance for required order input assert.match(source, /prop="shipperPartySnapshot\.contactName" required/) assert.match(source, /prop="consigneePartySnapshot\.address" required/) assert.match(source, /:prop="`items\.\$\{index\}\.cargoNameCn`" required/) + assert.match(source, /form\.destinationType === 'FBA' \|\| form\.fulfillmentType === 'ONE_CLICK_DROPSHIP' \? '必填' : '选填'/) + assert.match(source, /SKU NO\. 和 UPC NO\. 均为选填项/) + assert.match(source, /一件代发必须填写 SKU/) assert.match(source, /:prop="`items\.\$\{index\}\.grossWeightKg`" required/) assert.match(source, /

-

客户下单

先选择货物如何送到国内收货仓,并填写本次运输安排。

+
+

客户下单

创建新订单

普通头程一件代发测量完成后不办理国内库存入库,将直接推送 PDA 合托。 用于绑定领星入库单,提交后可在尾程订单、入库单和出库单中查询。 @@ -2213,7 +2238,7 @@ async function searchOrders(model: QueryModel) { - + @@ -2233,7 +2258,7 @@ async function searchOrders(model: QueryModel) {
#SKU NO.SKU必填中文品名图片英文品名报关 HS CODE清关 HS CODE包装类型材质危险品木制品勾选后须商检SKU NO.{{ form.destinationType === 'FBA' || form.fulfillmentType === 'ONE_CLICK_DROPSHIP' ? '必填' : '选填' }}中文品名图片英文品名报关 HS CODE清关 HS CODE包装类型材质危险品木制品勾选后须商检 箱规长 cm箱规宽 cm箱规高 cm每箱数量箱数总毛重总体积总数量总净重 清关申报单价 USD清关申报金额 USD用途型号品牌产品规格 长×宽×高货件编号货件编号必填跟踪编号跟踪编号必填UPC NO.备注操作
-
每个箱组默认装一个 SKU;同一箱内有多个 SKU 时点击“添加 SKU”,箱数、箱规、重量和体积只填写一次。SKU NO.、货件编号、跟踪编号为 FBA 货物必填项,UPC NO. 选填
+
一件代发必须填写 SKU;每个箱组默认装一个 SKU,同一箱内有多个 SKU 时点击“添加 SKU”,箱数、箱规、重量和体积只填写一次。SKU NO.、货件编号、跟踪编号为 FBA 货物必填项,UPC NO. 选填SKU NO. 和 UPC NO. 均为选填项
@@ -2271,7 +2296,7 @@ async function searchOrders(model: QueryModel) {
- + @@ -2528,8 +2553,23 @@ async function searchOrders(model: QueryModel) { .is-create-route { gap: 0; } .is-create-route .first-leg-editor-page { width: 100%; } .is-create-route .supporting-data-error, .is-create-route > .el-alert { margin-bottom: 14px; } -.is-create-route .prototype-order-form { width: 100%; margin: 0; padding: 0 0 16px; } -.is-create-route .prototype-section:not(.cargo-section):not(.cost-estimate-section):not(.destination-section) { width: min(100%, 900px); } +.is-create-route .prototype-order-form { display: grid; width: 100%; grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: stretch; gap: 16px; margin: 0; padding: 0 0 16px; } +.is-create-route .prototype-section { min-width: 0; margin: 0; padding: 0 24px 24px; border: 1px solid #dfe6ee; border-radius: 8px; background: #fff; } +.is-create-route .prototype-section + .prototype-section { margin-top: 0; } +.is-create-route .order-basic-section, .is-create-route .destination-section { height: 100%; } +.is-create-route .cargo-section, .is-create-route .cost-estimate-section { grid-column: 1 / -1; } +.is-create-route .order-section-heading { min-height: 66px; align-items: center; margin: 0 -24px 22px; padding: 15px 24px; border-bottom: 1px solid #e7ecf2; } +.is-create-route .order-section-heading h3, .is-create-route .prototype-section:first-child > .order-section-heading h3 { margin: 0; color: var(--navy); font-size: 17px; font-weight: 700; } +.is-create-route .order-section-heading p { margin-top: 4px; color: #8a98a8; } +.is-create-route .prototype-form-flow, .is-create-route .destination-form-flow { width: 100%; max-width: none; } +.is-create-route .prototype-form-row, .is-create-route .prototype-form-row--wide, .is-create-route .prototype-form-row--compact, .is-create-route .prototype-form-row--short, .is-create-route .prototype-form-row--tabs { grid-template-columns: 120px minmax(0, 1fr); } +.is-create-route .prototype-form-row :deep(.el-form-item__label) { display: flex; height: 40px; min-height: 40px; align-items: center; justify-content: flex-end; padding: 0 18px 0 0; font-size: 13px; line-height: 1.35; text-align: right; } +.is-create-route .segmented-options, .is-create-route .destination-type-options { width: 100%; max-width: 560px; background: #f3f6f9; } +.is-create-route .compact-transport-grid { width: 100%; margin-top: 4px; } +.is-create-route .compact-transport-grid > :deep(.el-form-item), .is-create-route .compact-transport-grid > :deep(.el-form-item:nth-child(1)), .is-create-route .compact-transport-grid > :deep(.el-form-item:nth-child(6)), .is-create-route .compact-transport-grid > :deep(.el-form-item:nth-child(7)) { grid-template-columns: 120px minmax(0, 1fr); } +.is-create-route .compact-transport-grid > :deep(.el-form-item__label) { display: flex; height: 40px; min-height: 40px; align-items: center; justify-content: flex-end; padding: 0 18px 0 0; line-height: 1.35; text-align: right; } +.is-create-route .selected-destination { width: auto; margin-left: 138px; } +.is-create-route .cargo-table-heading, .is-create-route .cost-estimate-section .order-section-heading { margin-bottom: 16px; } .order-form { width: min(100%, 1000px); margin: 0; padding: 0 0 12px; } .order-form.is-dialog-order-form { width: 100%; max-width: none; } .is-dialog-order-form > .cargo-section, .is-dialog-order-form > .cost-estimate-section { width: 100%; max-width: none; } @@ -2604,6 +2644,7 @@ async function searchOrders(model: QueryModel) { .compact-transport-grid :deep(.el-form-item__label) { color: #4d5a6c; font-size: 13px; } .shipping-form-flow { max-width: 900px; } .prototype-order-form > .prototype-section:first-child { width: 100%; } +.is-create-route .prototype-order-form > .prototype-section:first-child > .order-section-heading { margin-top: 0; } .shipping-form-flow .prototype-form-row { margin-bottom: 10px; } .shipping-form-flow .prototype-form-row--tabs { margin-bottom: 12px; } .compact-transport-grid { display: grid; width: 100%; grid-template-columns: 1fr; gap: 0; margin: 6px 0 0; } diff --git a/src/modules/overseas-warehouse/api/returnOrders.ts b/src/modules/overseas-warehouse/api/returnOrders.ts index a28af3e..c6fd930 100644 --- a/src/modules/overseas-warehouse/api/returnOrders.ts +++ b/src/modules/overseas-warehouse/api/returnOrders.ts @@ -1,18 +1,20 @@ -import { overseasApiRequest } from '@/api/overseasHttp' +import { overseasApiRequest, overseasBlobRequest } from '@/api/overseasHttp' import type { PageResult } from './warehouseServices' export interface ReturnOrderSummary { returnNo: string; overseasWarehouseId: string; warehouseContactName?: string; warehouseContactPhone?: string; customerReferenceNo?: string; status: string - skuTypes: number; expectedQuantity: number; receivedQuantity: number; needRelabel: boolean; createdAt: string + skuTypes: number; expectedQuantity: number; receivedQuantity: number; needRelabel: boolean; relabelNo?: string; relabelStatus?: string; createdAt: string } export interface ReturnOrderItem { skuCode: string; productNameZh?: string; expectedQuantity: number; receivedQuantity: number } export interface ReturnReceipt { - receiptNo: string; receiptBatchNo: string; cartonMark?: string; trackingNo?: string; startedBy: 'RETURN_NO' | 'SKU' + receiptNo: string; receiptBatchNo: string; palletCode: string; trackingNo?: string; startedBy: 'RETURN_NO' | 'SKU' receivedQuantity: number; operatorName?: string; signedAt: string } -export interface ReturnOrderDetail { order: ReturnOrderSummary; remark?: string; items: ReturnOrderItem[]; receipts: ReturnReceipt[] } +export interface ReturnPallet { palletCode: string; skuTypes: number; totalQuantity: number; items: Array<{ skuCode: string; productNameZh?: string; quantity: number }> } +export interface ReturnPhoto { fileId: number; fileName: string; contentType: string; sizeBytes: number } +export interface ReturnOrderDetail { order: ReturnOrderSummary; remark?: string; items: ReturnOrderItem[]; receipts: ReturnReceipt[]; pallets: ReturnPallet[]; photos: ReturnPhoto[] } 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 }> @@ -27,3 +29,21 @@ export function getReturnOrder(returnNo: string) { export function createReturnOrder(payload: ReturnOrderCreate) { return overseasApiRequest({ method: 'POST', url: '/merchant/v1/last-leg/return-orders', data: payload }) } +export function updateReturnOrder(returnNo: string, payload: ReturnOrderCreate) { + return overseasApiRequest({ method: 'PUT', url: `/merchant/v1/last-leg/return-orders/${encodeURIComponent(returnNo)}`, data: payload }) +} +export interface ReturnRelabelCreate { + fileScope: 'CARTON_TOTAL' | 'SKU_TOTAL'; destinationFbaWarehouseId: string; destinationFbaWarehouseCode: string; cartonRemark?: string; idempotencyKey: string + items: Array<{ skuCode: string; quantity: number }> +} +export interface ReturnRelabelCreated { relabelNo: string; relabelApplicationId: string; returnNo: string; skuCodes: string[]; status: string } +export function createReturnRelabel(returnNo: string, payload: ReturnRelabelCreate) { + return overseasApiRequest({ method: 'POST', url: `/merchant/v1/last-leg/return-orders/${encodeURIComponent(returnNo)}/relabel`, data: payload }) +} +export function uploadReturnRelabelFile(relabelNo: string, scope: 'CARTON_TOTAL' | 'SKU_TOTAL', file: File) { + const data = new FormData(); data.append('file', file) + return overseasApiRequest({ method: 'POST', url: `/merchant/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/files`, params: { scope }, data, headers: { 'Content-Type': 'multipart/form-data' } }) +} +export function downloadReturnPhoto(returnNo: string, fileId: string | number) { + return overseasBlobRequest({ method: 'GET', url: `/merchant/v1/last-leg/return-orders/${encodeURIComponent(returnNo)}/photos/files/${encodeURIComponent(String(fileId))}/download` }) +} diff --git a/src/modules/overseas-warehouse/api/warehouseServices.ts b/src/modules/overseas-warehouse/api/warehouseServices.ts index 1f7b2ca..c9e4656 100644 --- a/src/modules/overseas-warehouse/api/warehouseServices.ts +++ b/src/modules/overseas-warehouse/api/warehouseServices.ts @@ -29,7 +29,7 @@ export interface RelabelInventoryCandidate { inventoryBalanceId: string; inboundOrderId: string; inboundOrderNo?: string; inboundCode?: string overseasWarehouseId: string; overseasWarehouseCode?: string; overseasWarehouseName?: string destinationFbaWarehouseCode?: string; destinationFbaWarehouseName?: string; skuCode?: string; productNameZh?: string - availablePackages: number; version: number + availablePackages: number | string; version: number positions: Array<{ positionId: string; palletNo?: string; batchNo?: string; warehouseLocationCode?: string; availablePackages: number }> sourceType?: 'INBOUND' | 'RETURN_CARTON'; returnCartonId?: string; cartonMark?: string skuItems?: Array<{ skuCode: string; newSkuCode?: string; quantity: number; remark?: string }> diff --git a/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs b/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs index d13b0e0..39bc20e 100644 --- a/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs +++ b/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs @@ -3,6 +3,15 @@ import { readFileSync } from 'node:fs' import test from 'node:test' const view = readFileSync(new URL('./ReturnOrdersView.vue', import.meta.url), 'utf8') +const api = readFileSync(new URL('../api/returnOrders.ts', import.meta.url), 'utf8') + +test('退货签收照片通过客户鉴权下载接口加载并释放对象 URL', () => { + assert.match(api, /\/return-orders\/\$\{encodeURIComponent\(returnNo\)\}\/photos\/files\/\$\{encodeURIComponent\(String\(fileId\)\)\}\/download/) + assert.match(api, /overseasBlobRequest/) + assert.match(view, /URL\.createObjectURL\(await downloadReturnPhoto\(detailReturnNo\.value, photo\.fileId\)\)/) + assert.match(view, /URL\.revokeObjectURL\(url\)/) + assert.match(view, /onBeforeUnmount\(clearPhotoUrls\)/) +}) test('新建退货预报选择海外仓后带出联系人和电话', () => { assert.match(view, /@change="selectOperationWarehouse"/) @@ -18,7 +27,7 @@ test('新建退货预报选择海外仓后带出联系人和电话', () => { }) test('新建退货预报使用加宽的双列表单弹窗', () => { - assert.match(view, /title="新建退货预报" width="1120px"/) + assert.match(view, /:title="editingReturnNo \? '编辑退货预报' : '新建退货预报'" width="1120px"/) assert.match(view, //) assert.match(view, /return-create-form \{ display:grid; grid-template-columns:repeat\(2,minmax\(0,1fr\)\)/) }) @@ -28,10 +37,10 @@ test('退货列表和详情使用客户可读仓库、状态与时间', () => { assert.match(view, /return warehouse \? warehouseLabel\(warehouse\) : '未关联'/) assert.match(view, /PENDING_RECEIPT: '待签收'/) assert.match(view, /PARTIALLY_RECEIVED: '部分签收'/) - assert.match(view, /COMPLETED: '已完成'/) + assert.match(view, /return needRelabel \? '已完成入库\/待换标' : '已完成入库'/) assert.match(view, /function formatDateTime\(value\?: string \| null\)/) assert.match(view, /\{\{ operationWarehouseLabel\(row\.overseasWarehouseId\) \}\}/) - assert.match(view, /\{\{ statusLabel\(row\.status\) \}\}/) + assert.match(view, /\{\{ statusLabel\(row\.status, row\.needRelabel\) \}\}/) assert.match(view, /\{\{ formatDateTime\(row\.createdAt\) \}\}/) assert.doesNotMatch(view, / { test('可操作退货表格把操作列固定在最左侧', () => { const createTable = view.slice(view.indexOf('', view.indexOf(']*:row-actions="rowActions"/) assert.match(createTable, /^/) }) @@ -58,3 +67,13 @@ test('退货签收详情使用独立路由和紧凑连续页面', () => { assert.doesNotMatch(view, /showDetail/) assert.doesNotMatch(view, /class="return-detail-dialog"/) }) + +test('待审核退货可编辑,完成入库后从独立新增页发起贴标', () => { + assert.match(view, /row\.status === 'PENDING_REVIEW'.*key: 'edit'/) + assert.match(view, /await updateReturnOrder\(editingReturnNo\.value, payload\)/) + assert.match(view, /order\.status === 'COMPLETED' && order\.needRelabel === true && !order\.relabelNo/) + assert.match(view, /name: 'overseas-labeling-create'/) + assert.match(view, /query: \{ returnNo: order\.returnNo, warehouseId: String\(order\.overseasWarehouseId\) \}/) + assert.match(view, /prop: 'relabelNo', label: '换标单号'/) + assert.match(view, /label: '换标单状态'/) +}) diff --git a/src/modules/overseas-warehouse/views/ReturnOrdersView.vue b/src/modules/overseas-warehouse/views/ReturnOrdersView.vue index 1e4aa52..1c3b9b4 100644 --- a/src/modules/overseas-warehouse/views/ReturnOrdersView.vue +++ b/src/modules/overseas-warehouse/views/ReturnOrdersView.vue @@ -1,5 +1,5 @@ @@ -201,7 +269,7 @@ watch(() => [query.page, query.size] as const, ([nextPage, nextSize], [previousP
{{ selected.order.returnNo }}{{ operationWarehouseLabel(selected.order.overseasWarehouseId) }} · 退货签收
- {{ statusLabel(selected.order.status) }} +
贴标申请{{ statusLabel(selected.order.status, selected.order.needRelabel) }}

签收概览

@@ -209,6 +277,8 @@ watch(() => [query.page, query.size] as const, ([nextPage, nextSize], [previousP
实收 / 预报{{ selected.order.receivedQuantity }} / {{ selected.order.expectedQuantity }}
SKU 种类{{ selected.order.skuTypes }}
是否换标{{ selected.order.needRelabel ? '需要换标' : '否' }}
+
换标单号{{ selected.order.relabelNo || '—' }}
+
换标单状态{{ relabelStatusLabel(selected.order.relabelStatus) }}
客户参考号{{ selected.order.customerReferenceNo || '—' }}
海外仓联系人{{ selected.order.warehouseContactName || '—' }}
联系电话{{ selected.order.warehouseContactPhone || '—' }}
@@ -220,6 +290,31 @@ watch(() => [query.page, query.size] as const, ([nextPage, nextSize], [previousP

预报与实收

+
+

打托明细({{ selected.pallets?.length || 0 }} 托)

+ + + + + + + + + + + +
+
+

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

+
+ +
+ +

签收批次

@@ -237,6 +332,13 @@ watch(() => [query.page, query.size] as const, ([nextPage, nextSize], [previousP .return-orders-page { display:grid; gap:12px; } .return-create-form { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); column-gap:20px; } .return-form-wide { grid-column:1 / -1; } .items-title { display:flex; justify-content:space-between; align-items:center; margin:16px 0 4px; font-weight:600; } .items-hint { margin-bottom:10px; color:var(--el-text-color-secondary); font-size:12px; } h3 { margin:24px 0 10px; font-size:16px; } h4 { margin:16px 0 8px; font-size:14px; } .remark { white-space:pre-wrap; } .warehouse-error, .duplicate-sku-error { margin-top:6px; color:var(--el-color-danger); font-size:12px; } :deep(.duplicate-sku-input .el-input__wrapper) { box-shadow:0 0 0 1px var(--el-color-danger) inset; } :deep(.el-dialog .el-select) { width:100%; } .return-table-card { overflow:hidden; border:1px solid var(--client-border); border-radius:var(--client-radius); background:#fff; box-shadow:none; } .return-table-card :deep(.el-table) { width:100%; } +.return-status--pending-review { --el-tag-bg-color:#fff7e6; --el-tag-border-color:#f4c26b; --el-tag-text-color:#a86100; } +.return-status--pending-receipt { --el-tag-bg-color:#edf5ff; --el-tag-border-color:#8bbcff; --el-tag-text-color:#1764c0; } +.return-status--partially-received { --el-tag-bg-color:#f5efff; --el-tag-border-color:#b99aee; --el-tag-text-color:#6f42b5; } +.return-status--relabel { --el-tag-bg-color:#fff1e8; --el-tag-border-color:#f1a36f; --el-tag-text-color:#bd4d08; } +.return-status--completed { --el-tag-bg-color:#edf9f0; --el-tag-border-color:#8bd29b; --el-tag-text-color:#23823b; } +.return-status--rejected { --el-tag-bg-color:#fff0f0; --el-tag-border-color:#ee9696; --el-tag-text-color:#c73535; } +.return-status--cancelled { --el-tag-bg-color:#f2f4f7; --el-tag-border-color:#c7cdd6; --el-tag-text-color:#667085; } .return-pagination { display:flex; justify-content:flex-end; padding:14px 18px; border-top:1px solid #eef1f5; } .return-pagination :deep(.el-pagination) { margin:0; } .return-pagination-total { color:#526176; font-size:14px; } @@ -248,6 +350,7 @@ watch(() => [query.page, query.size] as const, ([nextPage, nextSize], [previousP .return-detail-title { display:flex; min-width:0; flex-direction:column; gap:3px; } .return-detail-title strong { overflow:hidden; color:#1f2d45; font-size:18px; text-overflow:ellipsis; white-space:nowrap; } .return-detail-title span { color:#7c899c; font-size:12px; } +.return-detail-actions { display:flex; align-items:center; gap:8px; } .return-detail-section + .return-detail-section { border-top:1px solid var(--client-border); } .return-detail-section > h3 { min-height:40px; margin:0; padding:11px 16px 9px; color:#26364f; font-size:14px; } .return-detail-grid { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); border-top:1px solid #edf1f5; } @@ -256,7 +359,14 @@ watch(() => [query.page, query.size] as const, ([nextPage, nextSize], [previousP .return-detail-grid strong,.return-detail-remark strong { min-width:0; overflow-wrap:anywhere; color:#2c3c55; font-size:13px; } .return-detail-remark { display:flex; align-items:flex-start; gap:10px; padding:10px 14px; background:#f8fafc; } .return-detail-section :deep(.data-table) { border:0; border-radius:0; } +.return-pallet-list { padding:0 16px 12px; } +.return-pallet-summary { margin-left:12px; color:#7c899c; font-size:12px; font-weight:400; } +.return-photo-grid { display:grid; grid-template-columns:repeat(5,minmax(0,1fr)); gap:12px; padding:0 16px 16px; } +.return-photo { min-width:0; overflow:hidden; padding:0; border:1px solid #dfe6ef; border-radius:6px; color:#526176; background:#fff; cursor:pointer; } +.return-photo img,.return-photo>span { display:flex; width:100%; height:120px; align-items:center; justify-content:center; object-fit:cover; background:#f4f7fb; } +.return-photo small { display:block; overflow:hidden; padding:7px; text-overflow:ellipsis; white-space:nowrap; } @media (max-width: 760px) { .return-create-form { grid-template-columns:1fr; } } @media (max-width: 1100px) { .return-detail-grid { grid-template-columns:repeat(2,minmax(0,1fr)); } } -@media (max-width: 720px) { .return-detail-header { grid-template-columns:1fr auto; } .return-back { grid-column:1/-1; } .return-pagination { padding:12px; } } +@media (max-width: 900px) { .return-photo-grid { grid-template-columns:repeat(3,minmax(0,1fr)); } } +@media (max-width: 720px) { .return-detail-header { grid-template-columns:1fr auto; } .return-back { grid-column:1/-1; } .return-pagination { padding:12px; } .return-photo-grid { grid-template-columns:repeat(2,minmax(0,1fr)); } } diff --git a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue index fd5addf..6c84011 100644 --- a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue +++ b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue @@ -25,6 +25,7 @@ import { type MerchantWarehouseDetail, } from '@/modules/first-leg/api/firstLeg' import { loadAllWarehouseRecords } from '@/modules/first-leg/utils/warehousePagination' +import { createReturnRelabel, uploadReturnRelabelFile } from '../api/returnOrders' type ServiceType = 'labeling' | 'transfer' | 'disposal' | 'relabel' type PageMode = 'list' | 'create' | 'edit' | 'detail' @@ -33,6 +34,7 @@ type RelabelTemplateType = 'carton' | 'sku' | 'mixed' interface CargoRow { id: number; included?: boolean; sku: string; name: string; quantity: number; availableQuantity?: number; sourceOrderId?: string; sourceInventoryBalanceId?: string; sourceInventoryPositionId?: string; sourceInboundOrderId?: string; returnCartonId?: string; sourceInboundOrderNo?: string; sourceInboundCode?: string; sourceWarehouseName?: string; palletNo?: string; warehouseLocationCode?: string; fnsku: string; shipmentNo: string; newSkuCode: string; shippingMark: string; customerCartonMarks: string; cartonMark: string; fbaCartonMark: string; destinationWarehouseId: string; remark: string } interface SkuRelabelRow { id: number; sourceRowId: number | null; cartonMark: string; originalSku: string; newSku: string; quantity: number; fbaCartonMark: string; destinationWarehouseId: string; remark: string } interface ImportedRelabelBinding extends SkuRelabelRow { templateType: RelabelTemplateType; selected: boolean; shipmentNo: string } +interface ReturnRelabelSkuRow { skuCode: string; productNameZh: string; availableQuantity: number; selected: boolean; quantity: number } interface ServiceConfig { title: string; createTitle: string; numberLabel: string; emptyText: string; columns: string[] } interface TransferCarton { cartonMark: string; customerCartonMark?: string; cartonIndex: number; totalCartons: number; status: string } @@ -100,11 +102,6 @@ const shippingLabelFile = ref(null) const skuRelabelRows = ref([]) const importedRelabelBindings = ref([]) const cartonDetailRows = computed(() => cargoRows.value.filter((row) => row.included)) -const labelingModeHint = computed(() => labelingMode.value === 'carton' - ? '只替换外箱箱唛,不修改箱内商品 SKU 标签;整张申请统一使用订单信息中的 FBA 目的仓。' - : labelingMode.value === 'sku' - ? '只替换箱内商品 SKU 标签,保留原箱唛;同一箱唛可以添加多条 SKU 明细。' - : '同一箱同时替换商品 SKU 标签和外箱箱唛;整张申请统一使用订单信息中的 FBA 目的仓。') const submittedRows = ref[]>([]) const page = ref(1) const pageSize = ref(15) @@ -168,17 +165,27 @@ const discardCandidatesLoading = ref(false) const relabelCandidates = ref([]) const selectedRelabelInboundOrderId = ref('') const relabelCandidatesLoading = ref(false) +const returnRelabelSkuRows = ref([]) +const isReturnRelabelSource = computed(() => props.service === 'labeling' && selectedRelabelInboundOrderId.value.startsWith('RETURN_ORDER:')) +const selectedReturnOrderNo = computed(() => isReturnRelabelSource.value ? selectedRelabelInboundOrderId.value.slice('RETURN_ORDER:'.length) : '') +const requiresBothTotalFiles = computed(() => (props.service === 'labeling' || (props.service === 'transfer' && form.relabel)) && !isReturnRelabelSource.value && labelingMode.value === 'carton_and_sku') +function inventoryQuantity(value: number | string | null | undefined) { + const quantity = Number(value) + return Number.isFinite(quantity) && quantity > 0 ? quantity : 0 +} const relabelInboundOptions = computed(() => { - const grouped = new Map() + const grouped = new Map() for (const candidate of relabelCandidates.value) { - const value = candidate.sourceType === 'RETURN_CARTON' ? `RETURN_CARTON:${candidate.returnCartonId}` : String(candidate.inboundOrderId) + const returnOrderNo = candidate.inboundOrderNo || String(candidate.returnCartonId || '') + const value = candidate.sourceType === 'RETURN_CARTON' ? `RETURN_ORDER:${returnOrderNo}` : String(candidate.inboundOrderId) const current = grouped.get(value) - if (current) current.availablePackages += candidate.availablePackages + if (current) current.availablePackages += inventoryQuantity(candidate.availablePackages) else grouped.set(value, { value, - inboundOrderNo: candidate.sourceType === 'RETURN_CARTON' ? `退货合箱 ${candidate.cartonMark || '—'}` : (candidate.inboundOrderNo || '—'), - inboundCode: candidate.sourceType === 'RETURN_CARTON' ? '仓库唛头库存' : (candidate.inboundCode || '—'), - availablePackages: candidate.availablePackages, + inboundOrderNo: candidate.sourceType === 'RETURN_CARTON' ? `退货单 ${candidate.inboundOrderNo || '—'}` : (candidate.inboundOrderNo || '—'), + inboundCode: candidate.sourceType === 'RETURN_CARTON' ? '已签收退货库存' : (candidate.inboundCode || '—'), + availablePackages: inventoryQuantity(candidate.availablePackages), + quantityUnit: candidate.sourceType === 'RETURN_CARTON' ? '件' : '箱', }) } return [...grouped.values()] @@ -256,14 +263,32 @@ function resetForm() { selectedDiscardOrderId.value = '' relabelCandidates.value = [] selectedRelabelInboundOrderId.value = '' + cartonTotalFile.value = null + skuTotalFile.value = null + shippingLabelFile.value = null + if (cartonTotalCreateInput.value) cartonTotalCreateInput.value.value = '' + if (skuTotalCreateInput.value) skuTotalCreateInput.value.value = '' transferDestinationType.value = 'NONE' selectedOperationWarehouse.value = null editingTransferNo.value = '' } async function openCreate() { resetForm(); mode.value = 'create' - if (props.service === 'labeling' || props.service === 'relabel' || props.service === 'transfer') void loadWarehouses() - if (props.service === 'labeling') return + if (props.service === 'labeling' || props.service === 'relabel' || props.service === 'transfer') await loadWarehouses() + if (props.service === 'labeling') { + const requestedReturnNo = String(route.query.returnNo || '').trim() + const requestedWarehouseId = String(route.query.warehouseId || '').trim() + if (requestedReturnNo && requestedWarehouseId && operationWarehouses.value.some(warehouse => String(warehouse.id) === requestedWarehouseId)) { + form.warehouse = requestedWarehouseId + await selectOperationWarehouse() + const sourceValue = `RETURN_ORDER:${requestedReturnNo}` + if (relabelInboundOptions.value.some(option => option.value === sourceValue)) { + selectedRelabelInboundOrderId.value = sourceValue + selectRelabelInventory(sourceValue) + } else ElMessage.warning(`退货单 ${requestedReturnNo} 暂无可申请贴标的库存`) + } + return + } if (props.service !== 'disposal') { addCargoRow(); return } } @@ -336,6 +361,36 @@ async function selectDiscardWarehouse() { } function createCargoRow(): CargoRow { return { id: ++cargoSequence, sku: '', name: '', quantity: 1, fnsku: '', shipmentNo: '', newSkuCode: '', shippingMark: '', customerCartonMarks: '', cartonMark: '', fbaCartonMark: '', destinationWarehouseId: '', remark: '' } } function addCargoRow() { cargoRows.value.push(createCargoRow()) } +function escapePrintHtml(value: string) { + return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''') +} +function printTransferDraftCartonMarks() { + if (props.service !== 'transfer') return + const validRows = cargoRows.value.filter((row) => row.sku.trim() && Number(row.quantity) > 0) + if (!validRows.length || validRows.some((row) => !Number.isInteger(Number(row.quantity)))) { + ElMessage.warning('请先填写商品 SKU 和对应的整数箱数'); return + } + const totalCartons = validRows.reduce((sum, row) => sum + Number(row.quantity), 0) + if (totalCartons > 500) { ElMessage.warning('单次最多打印 500 张箱唛'); return } + const batch = new Date().toISOString().replace(/\D/g, '').slice(2, 14) + const labels: Array<{ mark: string; sku: string; index: number }> = [] + let cartonIndex = 0 + for (const [rowIndex, row] of validRows.entries()) { + let marks = transferCustomerMarks(row) + if (marks.length !== Number(row.quantity)) { + marks = Array.from({ length: Number(row.quantity) }, (_, index) => `CT-${batch}-${String(rowIndex + 1).padStart(2, '0')}-${String(index + 1).padStart(3, '0')}`) + row.customerCartonMarks = marks.join('\n') + } + for (const mark of marks) labels.push({ mark, sku: row.sku.trim(), index: ++cartonIndex }) + } + const printWindow = window.open('', '_blank') + if (!printWindow) { ElMessage.warning('浏览器阻止了打印窗口,请允许弹窗后重试'); return } + printWindow.opener = null + const sections = labels.map((label) => `
仓储转运客户箱唛
${escapePrintHtml(label.mark)}
SKU:${escapePrintHtml(label.sku)}
${label.index} / ${labels.length}
`).join('') + printWindow.document.open() + printWindow.document.write(`仓储转运客户箱唛${sections}`) + printWindow.document.close(); printWindow.focus(); printWindow.print() +} function addTransferRelabelRow() { transferRelabelRows.value.push(createCargoRow()) } function clearTransferRelabelRows() { transferRelabelRows.value = [] @@ -364,8 +419,14 @@ function applyExistingCartonBinding(row: SkuRelabelRow) { if (!row.destinationWarehouseId) row.destinationWarehouseId = mapping.destinationWarehouseId } function selectShippingLabel(event: Event) { shippingLabelFile.value = (event.target as HTMLInputElement).files?.[0] || null } -function selectCartonTotalFile(event: Event) { cartonTotalFile.value = (event.target as HTMLInputElement).files?.[0] || null } -function selectSkuTotalFile(event: Event) { skuTotalFile.value = (event.target as HTMLInputElement).files?.[0] || null } +function selectCartonTotalFile(event: Event) { + cartonTotalFile.value = (event.target as HTMLInputElement).files?.[0] || null + if (cartonTotalFile.value && !requiresBothTotalFiles.value) clearCreateTotalFile('SKU_TOTAL') +} +function selectSkuTotalFile(event: Event) { + skuTotalFile.value = (event.target as HTMLInputElement).files?.[0] || null + if (skuTotalFile.value && !requiresBothTotalFiles.value) clearCreateTotalFile('CARTON_TOTAL') +} function clearCreateTotalFile(scope: 'CARTON_TOTAL' | 'SKU_TOTAL') { if (saving.value) return const file = scope === 'CARTON_TOTAL' ? cartonTotalFile : skuTotalFile @@ -662,7 +723,8 @@ function warehouseDisplay(value?: string) { function statusLabel(value: string) { if (props.service === 'labeling') { if (value === 'PROCESSING') return '处理中' - return ['CUSTOMER_CONFIRM_PENDING', 'READY_FOR_OUTBOUND', 'COMPLETED'].includes(value) ? '换标完成' : '待处理' + if (value === 'CUSTOMER_CONFIRM_PENDING') return '待客户确认' + return ['READY_FOR_OUTBOUND', 'COMPLETED'].includes(value) ? '换标完成' : '待处理' } if (props.service === 'transfer') { const transferLabels: Record = { @@ -736,11 +798,41 @@ function listRowActions(candidate: object): TableAction[] { if (canConfirmRelabel.value && props.service === 'labeling' && item.__status === 'CUSTOMER_CONFIRM_PENDING' && !item.__confirmedAt) actions.push({ key: 'confirm-relabel', label: '确认换标', run: () => confirmRelabelFromList(item['0']) }) return actions } +async function submitReturnRelabel() { + const items = returnRelabelSkuRows.value.filter(item => item.selected) + .map(item => ({ skuCode: item.skuCode, quantity: Number(item.quantity) })) + if (!selectedReturnOrderNo.value || !items.length) { ElMessage.warning('请至少选择一个需要贴标的 SKU'); return } + if (items.some(item => item.quantity < 1 || item.quantity > (returnRelabelSkuRows.value.find(row => row.skuCode === item.skuCode)?.availableQuantity || 0))) { + ElMessage.warning('本次贴标数量必须大于 0,且不能超过该 SKU 的库存总数'); return + } + const destination = fbaWarehouses.value.find(warehouse => warehouse.warehouseCode === form.destination) + if (!destination) { ElMessage.warning('请选择 FBA 目的仓'); return } + const selectedFile = cartonTotalFile.value || skuTotalFile.value + const fileScope = cartonTotalFile.value ? 'CARTON_TOTAL' as const : 'SKU_TOTAL' as const + if (!selectedFile) { ElMessage.warning('请上传箱唛总文件或 SKU 总文件(二选一)'); return } + try { await ElMessageBox.confirm(`确认提交退货单 ${selectedReturnOrderNo.value} 的贴标申请吗?`, '提交确认', { confirmButtonText: '提交', cancelButtonText: '取消' }) } catch { return } + saving.value = true + try { + const created = await createReturnRelabel(selectedReturnOrderNo.value, { + items, + fileScope, + destinationFbaWarehouseId: String(destination.id), + destinationFbaWarehouseCode: destination.warehouseCode, + cartonRemark: form.remark.trim() || undefined, + idempotencyKey: crypto.randomUUID(), + }) + await uploadReturnRelabelFile(created.relabelNo, fileScope, selectedFile) + ElMessage.success(`贴标申请 ${created.relabelNo} 已提交,等待审核`) + await router.push({ name: 'overseas-labeling' }) + } catch (error) { ElMessage.error(error instanceof Error ? error.message : '退货贴标申请提交失败,请重试') } + finally { saving.value = false } +} watch([page, pageSize], ([nextPage, nextSize], [previousPage, previousSize]) => { if (nextSize !== previousSize && nextPage !== 1) { page.value = 1; return } void load() }) async function submit(action: 'draft' | 'submit' = 'submit') { + if (isReturnRelabelSource.value) { await submitReturnRelabel(); return } const primaryWarehouse = props.service === 'relabel' ? form.sourceWarehouse : form.warehouse const relabelRows = labelingMode.value === 'carton' ? cartonDetailRows.value : skuRelabelRows.value const invalidCargo = props.service === 'disposal' @@ -757,6 +849,12 @@ async function submit(action: 'draft' | 'submit' = 'submit') { if (props.service === 'labeling' && (!form.labelDeadline || !form.contact || !form.phone)) { ElMessage.warning('请填写贴标截止时间和海外仓联系人信息'); return } + if (props.service === 'labeling' && requiresBothTotalFiles.value && (!cartonTotalFile.value || !skuTotalFile.value)) { + ElMessage.warning('SKU 和箱唛都换时,必须同时上传箱唛总文件和 SKU 总文件'); return + } + if (props.service === 'labeling' && !requiresBothTotalFiles.value && !cartonTotalFile.value && !skuTotalFile.value) { + ElMessage.warning('请上传箱唛总文件或 SKU 总文件(二选一)'); return + } if (props.service === 'labeling' && labelingMode.value !== 'sku' && !form.deliveryMethod) { ElMessage.warning('请选择换标完成后的派送方式'); return } @@ -792,6 +890,8 @@ async function submit(action: 'draft' | 'submit' = 'submit') { if (props.service === 'transfer' && form.relabel) { if (!form.labelDeadline || !form.relabelContact || !form.relabelPhone || (labelingMode.value !== 'sku' && !form.relabelDeliveryMethod)) { ElMessage.warning('请填写换标截止时间、换标联系人和换标后派送方式'); return } if (!transferRelabelDefaultDestination.value) { ElMessage.warning('请选择整票换标货物的 FBA 目的仓'); return } + if (action === 'submit' && requiresBothTotalFiles.value && (!cartonTotalFile.value || !skuTotalFile.value)) { ElMessage.warning('SKU 和箱唛都换时,必须同时上传箱唛总文件和 SKU 总文件'); return } + if (action === 'submit' && !requiresBothTotalFiles.value && !cartonTotalFile.value && !skuTotalFile.value) { ElMessage.warning('请上传箱唛总文件或 SKU 总文件(二选一)'); return } const relabelRows = transferRelabelRows.value const allMarks = relabelRows.flatMap(transferCustomerMarks) if (!relabelRows.length || relabelRows.some((row) => transferCustomerMarks(row).length !== row.quantity)) { ElMessage.warning('每个换标明细的客户箱唛数量必须与换标数量一致'); return } @@ -848,7 +948,19 @@ async function submit(action: 'draft' | 'submit' = 'submit') { const transferNo = String(created.transferNo || '') if (!transferNo) throw new Error('转运草稿已保存,但未返回转运单号') if (action === 'submit') { - await submitTransfer(transferNo) + const submitted = await submitTransfer(transferNo) + if (form.relabel) { + const relabelNo = String(submitted.relabelNo || created.relabelNo || '') + try { + if (!relabelNo) throw new Error('转运单已提交,但未返回关联换标单号') + if (cartonTotalFile.value) await uploadRelabelFile(relabelNo, 'CARTON_TOTAL', cartonTotalFile.value) + if (skuTotalFile.value) await uploadRelabelFile(relabelNo, 'SKU_TOTAL', skuTotalFile.value) + } catch (error) { + await ElMessageBox.alert(`转运单 ${transferNo} 已提交,但换标总文件上传失败:${error instanceof Error ? error.message : '上传失败'}。请进入关联换标单详情补传,不要重复提交转运单。`, '转运单已提交,附件待补传', { confirmButtonText: '知道了' }) + await openDetail({ '0': transferNo }) + return + } + } ElMessage.success('转运单已提交审核') await openDetail({ '0': transferNo }) return @@ -1044,15 +1156,31 @@ async function selectOperationWarehouse() { } function selectRelabelInventory(inboundOrderId: string) { const rows: CargoRow[] = [] - for (const candidate of relabelCandidates.value.filter((item) => (item.sourceType === 'RETURN_CARTON' ? `RETURN_CARTON:${item.returnCartonId}` : String(item.inboundOrderId)) === inboundOrderId)) { - if (candidate.sourceType === 'RETURN_CARTON') rows.push({ - id: ++cargoSequence, sku: candidate.skuCode || '', name: candidate.productNameZh || '退货合箱库存', - included: true, quantity: 1, availableQuantity: 1, returnCartonId: String(candidate.returnCartonId), - sourceInboundOrderNo: candidate.inboundOrderNo, sourceWarehouseName: candidate.overseasWarehouseName, - fnsku: '', shipmentNo: candidate.inboundOrderNo || candidate.cartonMark || '', newSkuCode: '', shippingMark: '', customerCartonMarks: '', - cartonMark: candidate.cartonMark || '', fbaCartonMark: '', destinationWarehouseId: candidate.destinationFbaWarehouseCode || '', remark: '', - }) - else for (const position of candidate.positions) rows.push(...Array.from({ length: Math.min(position.availablePackages, 500) }, () => ({ + const selectedCandidates = relabelCandidates.value.filter((item) => { + const returnOrderNo = item.inboundOrderNo || String(item.returnCartonId || '') + return (item.sourceType === 'RETURN_CARTON' ? `RETURN_ORDER:${returnOrderNo}` : String(item.inboundOrderId)) === inboundOrderId + }) + returnRelabelSkuRows.value = [] + if (inboundOrderId.startsWith('RETURN_ORDER:')) { + const grouped = new Map() + for (const candidate of selectedCandidates) { + const skuItems = candidate.skuItems?.length + ? candidate.skuItems + : candidate.skuCode ? [{ skuCode: candidate.skuCode, quantity: inventoryQuantity(candidate.availablePackages) }] : [] + for (const item of skuItems) { + const skuCode = item.skuCode.trim() + if (!skuCode) continue + const key = skuCode.toUpperCase() + const current = grouped.get(key) + const quantity = inventoryQuantity(item.quantity) + if (current) current.availableQuantity += quantity + else grouped.set(key, { skuCode, productNameZh: candidate.skuCode?.trim().toUpperCase() === key ? (candidate.productNameZh || '') : '', availableQuantity: quantity, selected: false, quantity }) + } + } + returnRelabelSkuRows.value = [...grouped.values()] + labelingMode.value = 'sku' + } else for (const candidate of selectedCandidates) { + for (const position of candidate.positions) rows.push(...Array.from({ length: Math.min(inventoryQuantity(position.availablePackages), 500) }, () => ({ id: ++cargoSequence, sku: candidate.skuCode || '', name: candidate.productNameZh || '库存货物', included: false, quantity: 1, availableQuantity: 1, sourceInventoryBalanceId: String(candidate.inventoryBalanceId), sourceInventoryPositionId: String(position.positionId), @@ -1070,12 +1198,12 @@ function selectRelabelInventory(inboundOrderId: string) { shippingLabelFile.value = null cartonTotalFile.value = null skuTotalFile.value = null - if (labelingMode.value !== 'carton') addSkuRelabelRow() + if (!isReturnRelabelSource.value && labelingMode.value !== 'carton') addSkuRelabelRow() }