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, /\*<\/span>总毛重/)
assert.match(source, /\*<\/span>总体积/)
diff --git a/src/modules/first-leg/views/FirstLegOrdersView.vue b/src/modules/first-leg/views/FirstLegOrdersView.vue
index a40039e..f7836c3 100644
--- a/src/modules/first-leg/views/FirstLegOrdersView.vue
+++ b/src/modules/first-leg/views/FirstLegOrdersView.vue
@@ -1262,8 +1262,12 @@ async function loadSelectedWarehouse(id: string | number | boolean | undefined)
async function autofillWarehouseSelections() {
if (!isCreatePage.value || editingId.value) return
- if (!form.receivingWarehouseId && availableWarehouses.value.length === 1) {
- form.receivingWarehouseId = availableWarehouses.value[0].id
+ if (!form.receivingWarehouseId) {
+ const yiwuWarehouse = availableWarehouses.value.find((warehouse) => warehouse.code.toUpperCase().includes('YIWU'))
+ ?? availableWarehouses.value.find((warehouse) => warehouse.name.includes('义乌'))
+ if (yiwuWarehouse) form.receivingWarehouseId = yiwuWarehouse.id
+ }
+ if (form.receivingWarehouseId) {
await loadSelectedWarehouse(form.receivingWarehouseId)
}
if (form.destinationType !== 'PRIVATE' && !form.destinationWarehouseId && availableDestinationWarehouses.value.length === 1) {
@@ -1485,6 +1489,27 @@ function closeEditor() {
})
}
+async function resetCreateForm() {
+ if (saving.value) return
+ if (editorDirty.value) {
+ try {
+ await ElMessageBox.confirm('确认清空当前填写的订单内容并恢复默认值?', '重置订单', {
+ type: 'warning',
+ confirmButtonText: '确认重置',
+ cancelButtonText: '继续填写',
+ })
+ } catch (error) {
+ if (error === 'cancel' || error === 'close') return
+ throw error
+ }
+ }
+ editingId.value = undefined
+ replaceForm(emptyDraft())
+ await autofillWarehouseSelections()
+ resetEditorValidation()
+ await router.replace({ name: 'first-leg-order-create' })
+}
+
function addCargoItem() {
form.items.push(createEmptyCargoItem(`new-cargo-${++cargoSequence}`))
resetEditorValidation()
@@ -1954,7 +1979,7 @@ async function submitOrder() {
const knownCompanyAuditStatus = companyAuditLoaded.value ? tenantStore.company.auditStatus : undefined
const savedOrder = await store.saveDraft(cloneDraft(form), tenantStore.company.companyName, editingId.value)
editingId.value = savedOrder.id
- const order = await store.submitSaved(savedOrder.id, knownCompanyAuditStatus)
+ await store.submitSaved(savedOrder.id, knownCompanyAuditStatus)
markEditorClean()
editorOpen.value = false
if (isCreatePage.value) {
@@ -1963,7 +1988,7 @@ async function submitOrder() {
ElMessage.success('订单已提交,等待平台审核')
return
}
- await router.push({ name: 'first-leg-order-detail', params: { orderId: order.id } })
+ await loadPageData()
ElMessage.success('订单已提交,等待平台审核')
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '订单提交失败')
@@ -2134,8 +2159,8 @@ async function searchOrders(model: QueryModel) {
-
- 返回保存草稿{{ companySubmissionBlocked ? '提交前需完成资料审核' : '提交订单' }}
+ 重置返回保存草稿{{ companySubmissionBlocked ? '提交前需完成资料审核' : '提交订单' }}
@@ -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 @@
@@ -170,12 +237,13 @@ watch(() => [query.page, query.size] as const, ([nextPage, nextSize], [previousP
新建退货预报
{{ operationWarehouseLabel(row.overseasWarehouseId) }}
需要换标否
- {{ statusLabel(row.status) }}
+ {{ statusLabel(row.status, row.needRelabel) }}
+ {{ relabelStatusLabel(row.relabelStatus) }}—
{{ formatDateTime(row.createdAt) }}
-
+
{{ operationWarehousesError }}
@@ -192,7 +260,7 @@ watch(() => [query.page, query.size] as const, ([nextPage, nextSize], [previousP
- 取消提交预报
+ 取消{{ editingReturnNo ? '保存修改' : '提交预报' }}
@@ -201,7 +269,7 @@ watch(() => [query.page, query.size] as const, ([nextPage, nextSize], [previousP
签收概览
@@ -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 }} 托)
+
+
+ {{ pallet.palletCode }}{{ pallet.skuTypes }} 种 SKU · {{ pallet.totalQuantity }} 件
+
+
+ {{ row.productNameZh || '—' }}
+
+
+
+
+
+
+
+ 退货打托照片({{ 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()
}
-
+
@@ -1089,17 +1217,17 @@ function selectRelabelInventory(inboundOrderId: string) {
{{ mode === 'edit' ? `编辑转运草稿 ${editingTransferNo}` : config.createTitle }}
返回列表