1Merge branch 'release/test' of http://8.163.30.128:10082/html/client-web into release/test
# Conflicts: # src/modules/overseas-warehouse/views/WarehouseServiceView.vue
This commit is contained in:
@@ -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<ReturnOrderDetail>({ method: 'POST', url: '/merchant/v1/last-leg/return-orders', data: payload })
|
||||
}
|
||||
export function updateReturnOrder(returnNo: string, payload: ReturnOrderCreate) {
|
||||
return overseasApiRequest<ReturnOrderDetail>({ 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<ReturnRelabelCreated>({ 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<unknown>({ 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` })
|
||||
}
|
||||
|
||||
@@ -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 }>
|
||||
|
||||
@@ -18,6 +18,28 @@ test('protects every overseas warehouse page with its independent list permissio
|
||||
assert.doesNotMatch(routes, /permission: 'dashboard:view'/)
|
||||
})
|
||||
|
||||
test('opens labeling and return relabeling on dedicated protected create pages', () => {
|
||||
assert.match(routes, /path: 'overseas-warehouse\/labeling\/create'[\s\S]*?props: \{ service: 'labeling', createPage: true \}[\s\S]*?permission: 'merchant:overseas-warehouse:relabel:create'/)
|
||||
assert.match(routes, /path: 'overseas-warehouse\/return-relabel\/create'[\s\S]*?ReturnOrdersView\.vue'[\s\S]*?props: \{ createPage: true \}[\s\S]*?title: '新建退货预报'[\s\S]*?permission: 'merchant:overseas-warehouse:return-order:create'/)
|
||||
assert.match(services, /props\.createPage \? 'create' : 'list'/)
|
||||
assert.match(services, /function returnToList\(\)/)
|
||||
assert.match(returns, /props\.createPage/)
|
||||
assert.match(returns, /class="return-create-surface"/)
|
||||
})
|
||||
|
||||
test('opens warehouse service and return receipt details as dedicated protected workspace pages', () => {
|
||||
assert.match(routes, /path: 'overseas-warehouse\/labeling\/:detailNo\/detail'[\s\S]*?name: 'overseas-labeling-detail'[\s\S]*?props: \{ service: 'labeling', detailPage: true \}[\s\S]*?permission: 'merchant:overseas-warehouse:relabel:list'/)
|
||||
assert.match(routes, /path: 'overseas-warehouse\/return-relabel\/:returnNo\/detail'[\s\S]*?name: 'overseas-return-relabel-detail'[\s\S]*?props: \{ detailPage: true \}[\s\S]*?permission: 'merchant:overseas-warehouse:return-order:list'/)
|
||||
assert.match(routes, /path: 'overseas-warehouse\/storage-transfer\/:detailNo\/detail'[\s\S]*?name: 'overseas-storage-transfer-detail'[\s\S]*?props: \{ service: 'transfer', detailPage: true \}/)
|
||||
assert.match(routes, /path: 'overseas-warehouse\/cargo-disposal\/:detailNo\/detail'[\s\S]*?name: 'overseas-cargo-disposal-detail'[\s\S]*?props: \{ service: 'disposal', detailPage: true \}/)
|
||||
assert.match(services, /name: 'overseas-labeling-detail'/)
|
||||
assert.match(services, /transfer: 'overseas-storage-transfer-detail'/)
|
||||
assert.match(services, /disposal: 'overseas-cargo-disposal-detail'/)
|
||||
assert.match(services, /class="warehouse-detail-error"/)
|
||||
assert.match(returns, /name: 'overseas-return-relabel-detail'/)
|
||||
assert.doesNotMatch(returns, /title="退货签收详情"/)
|
||||
})
|
||||
|
||||
test('keeps overseas warehouse writes behind operation-specific button permissions', () => {
|
||||
for (const permission of [
|
||||
'merchant:overseas-warehouse:relabel:create',
|
||||
@@ -37,6 +59,14 @@ test('keeps overseas warehouse writes behind operation-specific button permissio
|
||||
assert.match(returns, /v-if="canCreate" type="primary" @click="openCreate"/)
|
||||
})
|
||||
|
||||
test('keeps all four overseas warehouse create buttons in the shared left-aligned table toolbar', () => {
|
||||
for (const service of ['labeling', 'transfer', 'disposal']) {
|
||||
assert.match(routes, new RegExp(`props: \\{ service: '${service}' \\}`))
|
||||
}
|
||||
assert.match(services, /<DataTable[\s\S]*?<template #toolbar-actions><el-button v-if="canCreate"/)
|
||||
assert.match(returns, /<DataTable[\s\S]*?<template #toolbar-actions><el-button v-if="canCreate"/)
|
||||
})
|
||||
|
||||
test('publishes all overseas operation permissions in the enterprise role editor', () => {
|
||||
for (const permission of [
|
||||
'merchant:overseas-warehouse:last-mile-order:list',
|
||||
|
||||
@@ -1,22 +1,54 @@
|
||||
import { Boxes, PackageX, RefreshCcw, Tags, Truck, Warehouse } from '@lucide/vue'
|
||||
import { PackageX, RefreshCcw, Tags, Truck, Warehouse } from '@lucide/vue'
|
||||
import type { ClientModuleManifest } from '../types.ts'
|
||||
|
||||
export const overseasWarehouseModule = {
|
||||
key: 'overseas-warehouse',
|
||||
navigationGroup: { title: '海外仓', icon: Warehouse },
|
||||
routes: [
|
||||
{
|
||||
path: 'overseas-warehouse/labeling/create', name: 'overseas-labeling-create',
|
||||
component: () => import('./views/WarehouseServiceView.vue'), props: { service: 'labeling', createPage: true }, release: 'active',
|
||||
meta: { title: '新增贴标申请', workspaceTitle: '贴标申请 · 新建', description: '填写并提交海外仓贴标申请', permission: 'merchant:overseas-warehouse:relabel:create' },
|
||||
},
|
||||
{
|
||||
path: 'overseas-warehouse/labeling/:detailNo/detail', name: 'overseas-labeling-detail',
|
||||
component: () => import('./views/WarehouseServiceView.vue'),
|
||||
props: { service: 'labeling', detailPage: true }, release: 'active',
|
||||
meta: { title: '贴标申请详情', workspaceTitle: '贴标申请 · 详情', description: '查看贴标申请与仓库履约信息', permission: 'merchant:overseas-warehouse:relabel:list', keepAlive: false },
|
||||
},
|
||||
{
|
||||
path: 'overseas-warehouse/labeling', name: 'overseas-labeling',
|
||||
component: () => import('./views/WarehouseServiceView.vue'), props: { service: 'labeling' }, release: 'active',
|
||||
meta: { title: '贴标申请', description: '查询并管理海外仓贴标申请', permission: 'merchant:overseas-warehouse:relabel:list' },
|
||||
navigation: { key: 'overseas-labeling', icon: Tags },
|
||||
},
|
||||
{
|
||||
path: 'overseas-warehouse/return-relabel/create', name: 'overseas-return-relabel-create',
|
||||
component: () => import('./views/ReturnOrdersView.vue'), props: { createPage: true }, release: 'active',
|
||||
meta: { title: '新建退货预报', workspaceTitle: '退货预报 · 新建', description: '填写并提交海外仓退货预报', permission: 'merchant:overseas-warehouse:return-order:create', keepAlive: false },
|
||||
},
|
||||
{
|
||||
path: 'overseas-warehouse/return-relabel/:returnNo/detail', name: 'overseas-return-relabel-detail',
|
||||
component: () => import('./views/ReturnOrdersView.vue'),
|
||||
props: { detailPage: true }, release: 'active',
|
||||
meta: { title: '退货签收详情', workspaceTitle: '退货签收 · 详情', description: '查看退货预报与仓库签收结果', permission: 'merchant:overseas-warehouse:return-order:list', keepAlive: false },
|
||||
},
|
||||
{
|
||||
path: 'overseas-warehouse/storage-transfer/:detailNo/detail', name: 'overseas-storage-transfer-detail',
|
||||
component: () => import('./views/WarehouseServiceView.vue'), props: { service: 'transfer', detailPage: true }, release: 'active',
|
||||
meta: { title: '仓储转运详情', workspaceTitle: '仓储转运 · 详情', description: '查看仓储转运与入库测量详情', permission: 'merchant:overseas-warehouse:transfer:list', keepAlive: false },
|
||||
},
|
||||
{
|
||||
path: 'overseas-warehouse/storage-transfer', name: 'overseas-storage-transfer',
|
||||
component: () => import('./views/WarehouseServiceView.vue'), props: { service: 'transfer' }, release: 'active',
|
||||
meta: { title: '仓储转运', description: '查询并管理海外仓转运订单', permission: 'merchant:overseas-warehouse:transfer:list' },
|
||||
navigation: { key: 'overseas-storage-transfer', icon: Truck },
|
||||
},
|
||||
{
|
||||
path: 'overseas-warehouse/cargo-disposal/:detailNo/detail', name: 'overseas-cargo-disposal-detail',
|
||||
component: () => import('./views/WarehouseServiceView.vue'), props: { service: 'disposal', detailPage: true }, release: 'active',
|
||||
meta: { title: '货物废弃详情', workspaceTitle: '货物废弃 · 详情', description: '查看货物废弃申请详情', permission: 'merchant:overseas-warehouse:discard:list', keepAlive: false },
|
||||
},
|
||||
{
|
||||
path: 'overseas-warehouse/cargo-disposal', name: 'overseas-cargo-disposal',
|
||||
component: () => import('./views/WarehouseServiceView.vue'), props: { service: 'disposal' }, release: 'active',
|
||||
@@ -33,13 +65,16 @@ export const overseasWarehouseModule = {
|
||||
path: 'overseas-warehouse/return-inventory', name: 'overseas-return-inventory',
|
||||
component: () => import('./views/ReturnInventoryView.vue'), release: 'active',
|
||||
meta: { title: '退货库存', description: '按箱号和 SKU 查询海外仓退货实收库存', permission: 'merchant:overseas-warehouse:return-inventory:list' },
|
||||
navigation: { key: 'overseas-return-inventory', icon: Boxes },
|
||||
},
|
||||
{
|
||||
path: 'overseas-warehouse/return-cartons', name: 'overseas-return-cartons',
|
||||
component: () => import('./views/ReturnInventoryView.vue'), props: { cartonOnly: true }, release: 'active',
|
||||
meta: { title: '退货箱号管理', description: '查询签收及合箱生成的退货箱号和箱内库存', permission: 'merchant:overseas-warehouse:return-inventory:list' },
|
||||
navigation: { key: 'overseas-return-cartons', icon: Boxes },
|
||||
},
|
||||
{
|
||||
path: 'overseas-warehouse/return-cartons/:cartonMark/detail', name: 'overseas-return-carton-detail',
|
||||
component: () => import('./views/ReturnInventoryView.vue'), props: { cartonOnly: true, detailPage: true }, release: 'active',
|
||||
meta: { title: '退货箱明细', workspaceTitle: '退货箱号 · 详情', description: '查看退货箱号与箱内 SKU 明细', permission: 'merchant:overseas-warehouse:return-inventory:list', keepAlive: false },
|
||||
},
|
||||
],
|
||||
} satisfies ClientModuleManifest
|
||||
|
||||
@@ -4,19 +4,20 @@ import test from 'node:test'
|
||||
|
||||
const inventory = readFileSync(new URL('./ReturnInventoryView.vue', import.meta.url), 'utf8')
|
||||
const orders = readFileSync(new URL('./ReturnOrdersView.vue', import.meta.url), 'utf8')
|
||||
const routes = readFileSync(new URL('../routes.ts', import.meta.url), 'utf8')
|
||||
|
||||
test('desktop filters reserve six slots and inventory time and statuses have readable presentation', () => {
|
||||
assert.match(inventory, /grid-template-columns: repeat\(4, minmax\(150px, 1fr\)\) 130px auto/)
|
||||
test('desktop filters use the shared query grid and inventory time and statuses have readable presentation', () => {
|
||||
assert.match(inventory, /<QueryToolbar[^>]*layout="grid"/)
|
||||
assert.match(inventory, /label: '箱号 \/ 箱唛'/)
|
||||
assert.match(inventory, /white-space: nowrap/)
|
||||
assert.match(inventory, /cartonStatusLabel\(cartonDetail.status\)/)
|
||||
assert.match(inventory, /formatInventoryTime\(row.createdAt\)/)
|
||||
assert.match(inventory, /formatInventoryTime\(row.updatedAt\)/)
|
||||
assert.match(inventory, /formatInventoryTime\(row.updatedAt \|\| row.createdAt \|\| row.receivedAt\)/)
|
||||
})
|
||||
|
||||
test('inventory failures are visible and are not presented as empty inventory', () => {
|
||||
assert.match(inventory, /catch \(error\)/)
|
||||
assert.match(inventory, /<el-alert v-if="loadError"/)
|
||||
assert.equal((inventory.match(/:empty-text="loadError \?/g) || []).length, 3)
|
||||
assert.equal((inventory.match(/:empty-text="loadError \?/g) || []).length, 1)
|
||||
assert.match(inventory, /当前账号无权访问退货库存/)
|
||||
assert.match(inventory, /error\.requestId/)
|
||||
assert.match(inventory, /error\.traceId/)
|
||||
@@ -30,17 +31,14 @@ test('return pagination receives numeric totals from the wire format', () => {
|
||||
})
|
||||
|
||||
test('inventory actions lead both tables and timestamps describe inventory changes', () => {
|
||||
const tables = inventory.match(/<el-table\b[^>]*>[\s\S]*?<\/el-table>/g).slice(0, 3)
|
||||
for (const table of tables) {
|
||||
assert.match(table, /<el-table\b[^>]*>\s*<el-table-column label="操作"[^>]*fixed="left"/)
|
||||
}
|
||||
assert.match(inventory, /prop="updatedAt" label="库存更新时间"/)
|
||||
assert.equal((inventory.match(/label: '操作', slotName: 'actions'[^\n]*fixed: 'left'/g) || []).length, 3)
|
||||
assert.match(inventory, /label: '库存更新时间'/)
|
||||
assert.doesNotMatch(inventory, /实收并上架|最近入库时间/)
|
||||
})
|
||||
|
||||
test('carton management uses server carton pagination rather than deduplicating SKU pages', () => {
|
||||
assert.match(inventory, /if \(props.cartonOnly\) \{\s*const page = await listReturnInventoryCartons/)
|
||||
assert.match(inventory, /:data="cartonSummaryRows" row-key="cartonMark"/)
|
||||
assert.match(inventory, /props.cartonOnly \? cartonSummaryRows.value/)
|
||||
assert.match(inventory, /cartonSummaryRows.value = \[\]/)
|
||||
})
|
||||
|
||||
@@ -59,3 +57,11 @@ test('warehouse selectors and displays use the catalog instead of internal IDs',
|
||||
assert.match(inventory, /warehouse\.warehouseCode/)
|
||||
assert.doesNotMatch(inventory, /海外仓 ID|\{\{ cartonDetail.overseasWarehouseId \}\}|prop="overseasWarehouseId"/)
|
||||
})
|
||||
|
||||
test('opens carton details on a dedicated compact page with a visible retry state', () => {
|
||||
assert.match(routes, /path: 'overseas-warehouse\/return-cartons\/:cartonMark\/detail'/)
|
||||
assert.match(inventory, /name: 'overseas-return-carton-detail'/)
|
||||
assert.match(inventory, /class="carton-detail-surface"/)
|
||||
assert.match(inventory, /detailError \|\| '箱号详情加载失败'/)
|
||||
assert.doesNotMatch(inventory, /<el-drawer/)
|
||||
})
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Printer, Search } from '@lucide/vue'
|
||||
import { ArrowLeft, Printer } from '@lucide/vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
||||
import type { TableAction } from '@/components/common/tableActions'
|
||||
import { ApiError } from '@/api/types'
|
||||
import { getMerchantWarehouses, type MerchantWarehouse } from '@/modules/first-leg/public'
|
||||
import {
|
||||
@@ -17,10 +21,14 @@ import {
|
||||
} from '../api/returnInventory'
|
||||
|
||||
const activeTab = ref<'summary' | 'cartons'>('summary')
|
||||
const props = defineProps<{ cartonOnly?: boolean }>()
|
||||
const props = defineProps<{ cartonOnly?: boolean; detailPage?: boolean }>()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const detailCartonMark = computed(() => String(route.params.cartonMark || ''))
|
||||
if (props.cartonOnly) activeTab.value = 'cartons'
|
||||
const loading = ref(false)
|
||||
const loadError = ref('')
|
||||
const detailError = ref('')
|
||||
const warehouses = ref<MerchantWarehouse[]>([])
|
||||
const warehouseError = ref('')
|
||||
const warehousesLoading = ref(false)
|
||||
@@ -50,9 +58,53 @@ const summaryRows = ref<ReturnInventorySummaryItem[]>([])
|
||||
const cartonRows = ref<ReturnInventoryItem[]>([])
|
||||
const cartonSummaryRows = ref<ReturnInventoryCartonSummary[]>([])
|
||||
const total = ref(0)
|
||||
const showCartonDetail = ref(false)
|
||||
const cartonDetail = ref<ReturnInventoryCartonDetail>()
|
||||
const query = reactive({ page: 1, size: 20, overseasWarehouseId: '', skuCode: '', cartonMark: '', status: 'AVAILABLE', returnNo: '' })
|
||||
const queryModel = computed<QueryModel>({
|
||||
get: () => ({ ...query }),
|
||||
set: (value) => Object.assign(query, value),
|
||||
})
|
||||
const queryFields = computed<QueryField[]>(() => {
|
||||
const fields: QueryField[] = [
|
||||
{ key: 'overseasWarehouseId', label: '海外仓', type: 'select', options: warehouses.value.map((warehouse) => ({ label: warehouseLabel(String(warehouse.id)), value: String(warehouse.id) })) },
|
||||
{ key: 'skuCode', label: 'SKU', type: 'input', placeholder: '请输入 SKU' },
|
||||
]
|
||||
if (activeTab.value === 'cartons') fields.push(
|
||||
{ key: 'cartonMark', label: '箱号 / 箱唛', type: 'input', placeholder: '请输入箱号或箱唛' },
|
||||
{ key: 'returnNo', label: '退货单号', type: 'input', placeholder: '请输入退货单号' },
|
||||
{ key: 'status', label: '箱状态', type: 'select', options: [{ label: '可用', value: 'AVAILABLE' }, { label: '已合箱', value: 'MERGED' }] },
|
||||
)
|
||||
return fields
|
||||
})
|
||||
const summaryColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', slotName: 'actions', width: 110, fixed: 'left' },
|
||||
{ key: 'warehouse', label: '海外仓', slotName: 'warehouse', minWidth: 190 },
|
||||
{ prop: 'skuCode', label: 'SKU', minWidth: 190 }, { prop: 'productNameZh', label: '中文品名', minWidth: 190 },
|
||||
{ prop: 'availableQuantity', label: '可用库存', width: 110, align: 'right' }, { prop: 'cartonQuantity', label: '所在箱数', width: 110, align: 'right' },
|
||||
{ key: 'time', label: '库存更新时间', slotName: 'time', width: 180 },
|
||||
]
|
||||
const cartonItemColumns: DataTableColumn[] = [
|
||||
{ prop: 'skuCode', label: 'SKU', minWidth: 200 },
|
||||
{ prop: 'productNameZh', label: '中文品名', minWidth: 240 },
|
||||
{ prop: 'quantity', label: '数量', width: 110, align: 'right', headerAlign: 'right' },
|
||||
]
|
||||
const cartonSummaryColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', slotName: 'actions', width: 150, fixed: 'left' }, { prop: 'cartonMark', label: '箱号', minWidth: 220 },
|
||||
{ key: 'warehouse', label: '海外仓', slotName: 'warehouse', minWidth: 190 }, { prop: 'skuTypes', label: 'SKU 种类', width: 110, align: 'right' },
|
||||
{ prop: 'totalQuantity', label: '总件数', width: 100, align: 'right' }, { prop: 'sourceReturnNo', label: '来源退货单', minWidth: 210 },
|
||||
{ prop: 'sourceBatchNo', label: '签收批次', minWidth: 180 }, { key: 'dimensions', label: '箱规', slotName: 'dimensions', width: 170 },
|
||||
{ key: 'weight', label: '重量', slotName: 'weight', width: 100 }, { key: 'time', label: '生成时间', slotName: 'time', width: 180 },
|
||||
]
|
||||
const cartonColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', slotName: 'actions', width: 150, fixed: 'left' }, { prop: 'cartonMark', label: '箱号', minWidth: 220 },
|
||||
{ key: 'warehouse', label: '海外仓', slotName: 'warehouse', minWidth: 190 }, { prop: 'skuCode', label: 'SKU', minWidth: 180 },
|
||||
{ prop: 'productNameZh', label: '中文品名', minWidth: 180 }, { prop: 'quantity', label: '本箱数量', width: 110, align: 'right' },
|
||||
{ prop: 'cartonSkuTypes', label: 'SKU 种类', width: 110, align: 'right' }, { prop: 'cartonTotalQuantity', label: '总件数', width: 100, align: 'right' },
|
||||
{ key: 'status', label: '箱状态', slotName: 'status', width: 100 }, { prop: 'sourceBatchNo', label: '签收批次', minWidth: 180 },
|
||||
{ key: 'time', label: '生成时间', slotName: 'time', width: 180 },
|
||||
]
|
||||
const currentRows = computed<object[]>(() => activeTab.value === 'summary' ? summaryRows.value : props.cartonOnly ? cartonSummaryRows.value : cartonRows.value)
|
||||
const currentColumns = computed(() => activeTab.value === 'summary' ? summaryColumns : props.cartonOnly ? cartonSummaryColumns : cartonColumns)
|
||||
|
||||
function params(includeCartonMark: boolean) {
|
||||
return {
|
||||
@@ -100,7 +152,8 @@ async function load() {
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
function search() { query.page = 1; void load() }
|
||||
function search() { if (query.page === 1) void load(); else query.page = 1 }
|
||||
function resetSearch() { query.status = 'AVAILABLE'; search() }
|
||||
function changeTab() { query.page = 1; void load() }
|
||||
function openSkuCartons(skuCode: string) {
|
||||
query.skuCode = skuCode
|
||||
@@ -110,11 +163,18 @@ function openSkuCartons(skuCode: string) {
|
||||
}
|
||||
|
||||
async function openCarton(cartonMark: string) {
|
||||
try {
|
||||
cartonDetail.value = await getReturnInventoryCarton(cartonMark)
|
||||
showCartonDetail.value = true
|
||||
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '箱内明细加载失败') }
|
||||
void router.push({ name: 'overseas-return-carton-detail', params: { cartonMark } })
|
||||
}
|
||||
async function loadCartonDetail() {
|
||||
if (!detailCartonMark.value) return
|
||||
loading.value = true
|
||||
detailError.value = ''
|
||||
try {
|
||||
cartonDetail.value = await getReturnInventoryCarton(detailCartonMark.value)
|
||||
} catch (error) { cartonDetail.value = undefined; detailError.value = error instanceof Error ? error.message : '箱内明细加载失败'; ElMessage.error(detailError.value) }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function returnToCartons() { void router.push({ name: 'overseas-return-cartons' }) }
|
||||
|
||||
async function printLabel(cartonMark: string) {
|
||||
const printWindow = window.open('', '_blank')
|
||||
@@ -136,59 +196,54 @@ async function printLabel(cartonMark: string) {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => { void load(); void loadWarehouses() })
|
||||
function rowActions(candidate: object): TableAction[] {
|
||||
const row = candidate as ReturnInventorySummaryItem | ReturnInventoryItem | ReturnInventoryCartonSummary
|
||||
if (activeTab.value === 'summary') return [{ key: 'cartons', label: '查看箱号', run: () => openSkuCartons((row as ReturnInventorySummaryItem).skuCode) }]
|
||||
const cartonMark = (row as ReturnInventoryItem | ReturnInventoryCartonSummary).cartonMark
|
||||
return [
|
||||
{ key: 'detail', label: '详情', run: () => openCarton(cartonMark) },
|
||||
{ key: 'label', label: '箱唛', run: () => printLabel(cartonMark) },
|
||||
]
|
||||
}
|
||||
|
||||
watch(() => [query.page, query.size] as const, ([nextPage, nextSize], [previousPage, previousSize]) => {
|
||||
if (nextSize !== previousSize && nextPage !== 1) { query.page = 1; return }
|
||||
void load()
|
||||
})
|
||||
|
||||
onMounted(() => { if (props.detailPage) void Promise.all([loadCartonDetail(), loadWarehouses()]); else { void load(); void loadWarehouses() } })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="return-inventory-page">
|
||||
<div class="page-heading"><div><h2>{{ props.cartonOnly ? '退货箱号管理' : '退货库存管理' }}</h2><p>{{ props.cartonOnly ? '查询签收及合箱生成的退货箱号、箱规和箱内 SKU 明细。' : '库存数量以仓库 PDA 确认实收后的入账数量为准。' }}</p></div></div>
|
||||
<template v-if="!props.detailPage">
|
||||
<el-tabs v-if="!props.cartonOnly" v-model="activeTab" @tab-change="changeTab"><el-tab-pane label="SKU 汇总库存" name="summary" /><el-tab-pane label="箱号明细" name="cartons" /></el-tabs>
|
||||
<div class="filter-bar" :class="{ 'carton-filters': activeTab === 'cartons' }">
|
||||
<el-select v-model="query.overseasWarehouseId" clearable filterable placeholder="海外仓" :loading="warehousesLoading"><el-option v-for="warehouse in warehouses" :key="warehouse.id" :label="warehouseLabel(String(warehouse.id))" :value="String(warehouse.id)" /></el-select>
|
||||
<el-input v-model="query.skuCode" clearable placeholder="SKU" @keyup.enter="search" />
|
||||
<el-input v-if="activeTab === 'cartons'" v-model="query.cartonMark" clearable placeholder="箱号 / 箱唛" @keyup.enter="search" />
|
||||
<el-input v-if="activeTab === 'cartons'" v-model="query.returnNo" clearable placeholder="退货单号" @keyup.enter="search" />
|
||||
<el-select v-if="activeTab === 'cartons'" v-model="query.status" clearable placeholder="箱状态"><el-option label="可用" value="AVAILABLE" /><el-option label="已合箱" value="MERGED" /></el-select>
|
||||
<el-button type="primary" @click="search"><Search :size="16" />查询</el-button>
|
||||
</div>
|
||||
<QueryToolbar v-model="queryModel" layout="grid" :ariaLabel="`${props.cartonOnly ? '退货箱号' : '退货库存'}筛选`" :fields="queryFields" :search-on-reset="false" @search="search" @reset="resetSearch" />
|
||||
<el-alert v-if="loadError" :title="loadError" type="error" show-icon :closable="false" />
|
||||
<el-alert v-if="warehouseError" :title="warehouseError" type="warning" show-icon :closable="false"><el-button link type="primary" @click="loadWarehouses">重试加载海外仓</el-button></el-alert>
|
||||
<el-table v-if="activeTab === 'summary'" v-loading="loading" :data="summaryRows" :empty-text="loadError ? '库存加载失败,请核对权限或重试' : '暂无数据'">
|
||||
<el-table-column label="操作" width="110" fixed="left"><template #default="{ row }"><el-button link type="primary" @click="openSkuCartons(row.skuCode)">查看箱号</el-button></template></el-table-column>
|
||||
<el-table-column label="海外仓" min-width="150"><template #default="{ row }">{{ warehouseLabel(row.overseasWarehouseId) }}</template></el-table-column><el-table-column prop="skuCode" label="SKU" min-width="180" />
|
||||
<el-table-column prop="productNameZh" label="中文品名" min-width="190" show-overflow-tooltip /><el-table-column prop="availableQuantity" label="可用库存" min-width="120" align="right" />
|
||||
<el-table-column prop="cartonQuantity" label="所在箱数" min-width="120" align="right" /><el-table-column prop="updatedAt" label="库存更新时间" min-width="180"><template #default="{ row }"><span class="inventory-time">{{ formatInventoryTime(row.updatedAt) }}</span></template></el-table-column>
|
||||
</el-table>
|
||||
<el-table v-else-if="props.cartonOnly" v-loading="loading" :data="cartonSummaryRows" row-key="cartonMark" :empty-text="loadError ? '库存加载失败,请核对权限或重试' : '暂无数据'">
|
||||
<el-table-column label="操作" width="150" fixed="left"><template #default="{ row }"><el-button link type="primary" @click="openCarton(row.cartonMark)">详情</el-button><el-button link type="primary" @click="printLabel(row.cartonMark)"><Printer :size="14" />箱唛</el-button></template></el-table-column>
|
||||
<el-table-column prop="cartonMark" label="箱号" min-width="235" />
|
||||
<el-table-column label="海外仓" min-width="150"><template #default="{ row }">{{ warehouseLabel(row.overseasWarehouseId) }}</template></el-table-column>
|
||||
<el-table-column prop="skuTypes" label="箱内 SKU 种类" min-width="130" align="right" />
|
||||
<el-table-column prop="totalQuantity" label="箱内总件数" min-width="115" align="right" />
|
||||
<el-table-column prop="sourceReturnNo" label="来源退货单" min-width="235" />
|
||||
<el-table-column prop="sourceBatchNo" label="签收批次" min-width="190" />
|
||||
<el-table-column label="箱规" min-width="160"><template #default="{ row }">{{ row.length && row.width && row.height ? `${row.length} × ${row.width} × ${row.height} cm` : '—' }}</template></el-table-column>
|
||||
<el-table-column label="重量" min-width="100"><template #default="{ row }">{{ row.weight ? `${row.weight} kg` : '—' }}</template></el-table-column>
|
||||
<el-table-column prop="createdAt" label="箱号生成时间" min-width="180"><template #default="{ row }"><span class="inventory-time">{{ formatInventoryTime(row.createdAt) }}</span></template></el-table-column>
|
||||
</el-table>
|
||||
<el-table v-else v-loading="loading" :data="cartonRows" :empty-text="loadError ? '库存加载失败,请核对权限或重试' : '暂无数据'">
|
||||
<el-table-column label="操作" width="150" fixed="left"><template #default="{ row }"><el-button link type="primary" @click="openCarton(row.cartonMark)">详情</el-button><el-button link type="primary" @click="printLabel(row.cartonMark)"><Printer :size="14" />箱唛</el-button></template></el-table-column>
|
||||
<el-table-column prop="cartonMark" label="箱号" min-width="235"><template #default="{ row }"><el-button link type="primary" @click="openCarton(row.cartonMark)">{{ row.cartonMark }}</el-button></template></el-table-column>
|
||||
<el-table-column label="海外仓" min-width="150"><template #default="{ row }">{{ warehouseLabel(row.overseasWarehouseId) }}</template></el-table-column><el-table-column prop="skuCode" label="SKU" min-width="150" />
|
||||
<el-table-column prop="productNameZh" label="中文品名" min-width="170" show-overflow-tooltip /><el-table-column prop="quantity" label="本箱 SKU 数量" min-width="135" align="right" />
|
||||
<el-table-column prop="cartonSkuTypes" label="箱内 SKU 种类" min-width="130" align="right" /><el-table-column prop="cartonTotalQuantity" label="箱内总件数" min-width="115" align="right" />
|
||||
<el-table-column prop="status" label="箱状态" min-width="100"><template #default="{ row }">{{ cartonStatusLabel(row.status) }}</template></el-table-column><el-table-column prop="sourceBatchNo" label="签收批次" min-width="190" /><el-table-column prop="receivedAt" label="箱号生成时间" min-width="180"><template #default="{ row }"><span class="inventory-time">{{ formatInventoryTime(row.receivedAt) }}</span></template></el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="query.page" v-model:page-size="query.size" :total="total" layout="total, prev, pager, next" @current-change="load" />
|
||||
<el-drawer v-model="showCartonDetail" title="退货箱明细" size="680px"><template v-if="cartonDetail">
|
||||
<el-descriptions :column="2" border><el-descriptions-item label="箱号">{{ cartonDetail.cartonMark }}</el-descriptions-item><el-descriptions-item label="海外仓">{{ warehouseLabel(cartonDetail.overseasWarehouseId) }}</el-descriptions-item><el-descriptions-item label="来源退货单">{{ cartonDetail.sourceReturnNo || '—' }}</el-descriptions-item><el-descriptions-item label="签收批次">{{ cartonDetail.sourceBatchNo || '—' }}</el-descriptions-item><el-descriptions-item label="状态">{{ cartonStatusLabel(cartonDetail.status) }}</el-descriptions-item><el-descriptions-item label="SKU 种类">{{ cartonDetail.skuTypes }}</el-descriptions-item><el-descriptions-item label="箱内总件数">{{ cartonDetail.totalQuantity }}</el-descriptions-item><el-descriptions-item label="箱规">{{ cartonDetail.length && cartonDetail.width && cartonDetail.height ? `${cartonDetail.length} × ${cartonDetail.width} × ${cartonDetail.height} cm` : '—' }}</el-descriptions-item><el-descriptions-item label="重量">{{ cartonDetail.weight ? `${cartonDetail.weight} kg` : '—' }}</el-descriptions-item></el-descriptions>
|
||||
<div class="drawer-heading"><h3>箱内 SKU</h3><el-button type="primary" @click="printLabel(cartonDetail.cartonMark)"><Printer :size="16" />打印箱唛</el-button></div>
|
||||
<el-table :data="cartonDetail.items" size="small"><el-table-column prop="skuCode" label="SKU" min-width="170" /><el-table-column prop="productNameZh" label="中文品名" min-width="180" /><el-table-column prop="quantity" label="数量" min-width="100" align="right" /></el-table>
|
||||
</template></el-drawer>
|
||||
<div class="return-inventory-table">
|
||||
<DataTable v-model:page="query.page" v-model:page-size="query.size" :data="currentRows" :columns="currentColumns" :loading="loading" :total="total" :page-sizes="[20, 50, 100]" :row-actions="rowActions" :empty-text="loadError ? '库存加载失败,请核对权限或重试' : '暂无数据'">
|
||||
<template #warehouse="{ row }">{{ warehouseLabel(row.overseasWarehouseId) }}</template>
|
||||
<template #status="{ row }"><el-tag effect="light">{{ cartonStatusLabel(row.status) }}</el-tag></template>
|
||||
<template #dimensions="{ row }">{{ row.length && row.width && row.height ? `${row.length} × ${row.width} × ${row.height} cm` : '—' }}</template>
|
||||
<template #weight="{ row }">{{ row.weight ? `${row.weight} kg` : '—' }}</template>
|
||||
<template #time="{ row }"><span class="inventory-time">{{ formatInventoryTime(row.updatedAt || row.createdAt || row.receivedAt) }}</span></template>
|
||||
</DataTable>
|
||||
</div>
|
||||
</template>
|
||||
<article v-else v-loading="loading" class="carton-detail-surface">
|
||||
<template v-if="cartonDetail">
|
||||
<header class="carton-detail-header"><button type="button" class="carton-detail-back" @click="returnToCartons"><ArrowLeft :size="16" />返回列表</button><div><strong>{{ cartonDetail.cartonMark }}</strong><span>{{ warehouseLabel(cartonDetail.overseasWarehouseId) }} · 退货箱明细</span></div><div class="carton-detail-actions"><el-tag effect="light">{{ cartonStatusLabel(cartonDetail.status) }}</el-tag><el-button type="primary" @click="printLabel(cartonDetail.cartonMark)"><Printer :size="16" />打印箱唛</el-button></div></header>
|
||||
<section class="carton-detail-section"><h3>箱号概览</h3><dl class="carton-detail-grid"><div><dt>来源退货单</dt><dd>{{ cartonDetail.sourceReturnNo || '—' }}</dd></div><div><dt>签收批次</dt><dd>{{ cartonDetail.sourceBatchNo || '—' }}</dd></div><div><dt>SKU 种类</dt><dd>{{ cartonDetail.skuTypes }}</dd></div><div><dt>箱内总件数</dt><dd>{{ cartonDetail.totalQuantity }}</dd></div><div><dt>箱规</dt><dd>{{ cartonDetail.length && cartonDetail.width && cartonDetail.height ? `${cartonDetail.length} × ${cartonDetail.width} × ${cartonDetail.height} cm` : '—' }}</dd></div><div><dt>重量</dt><dd>{{ cartonDetail.weight ? `${cartonDetail.weight} kg` : '—' }}</dd></div><div><dt>签收时间</dt><dd>{{ formatInventoryTime(cartonDetail.receivedAt) }}</dd></div></dl></section>
|
||||
<section class="carton-detail-section"><h3>箱内 SKU</h3><DataTable :data="cartonDetail.items" :columns="cartonItemColumns" row-key="skuCode" empty-text="暂无箱内 SKU" /></section>
|
||||
</template>
|
||||
<div v-else class="carton-detail-error" role="alert"><strong>{{ detailError || '箱号详情加载失败' }}</strong><el-button @click="loadCartonDetail">重新加载</el-button></div>
|
||||
</article>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.return-inventory-page { padding: 20px; } .page-heading { margin-bottom: 14px; } h2 { margin: 0; font-size: 22px; } p { margin: 6px 0 0; color: var(--el-text-color-secondary); } .filter-bar { display: grid; grid-template-columns: repeat(2, minmax(180px, 320px)) auto; justify-content: start; gap: 10px; margin-bottom: 14px; } .filter-bar.carton-filters { grid-template-columns: repeat(4, minmax(150px, 1fr)) 130px auto; } .filter-bar > * { min-width: 0; } .inventory-time { white-space: nowrap; } .el-pagination { margin-top: 16px; justify-content: flex-end; } .drawer-heading { display: flex; align-items: center; justify-content: space-between; margin: 24px 0 12px; } .drawer-heading h3 { margin: 0; font-size: 16px; }
|
||||
@media (max-width: 760px) { .return-inventory-page { padding: 12px; } .filter-bar, .filter-bar.carton-filters { grid-template-columns: 1fr 1fr; } }
|
||||
.return-inventory-page { display: grid; gap: 12px; } .return-inventory-page :deep(.el-tabs__header) { margin: 0; } .return-inventory-table { overflow: hidden; border: 1px solid var(--client-border); border-radius: var(--client-radius); background: #fff; } .inventory-time { white-space: nowrap; }
|
||||
.carton-detail-surface{overflow:hidden;min-height:240px;border:1px solid var(--client-border);border-radius:var(--client-radius);background:#fff}.carton-detail-header{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:14px;min-height:64px;padding:10px 16px;border-bottom:1px solid var(--client-border)}.carton-detail-back{display:inline-flex;align-items:center;gap:5px;padding:5px 0;border:0;background:transparent;color:var(--el-color-primary);cursor:pointer}.carton-detail-header>div:nth-child(2){display:flex;min-width:0;flex-direction:column;gap:3px}.carton-detail-header strong{overflow:hidden;color:#1f2d45;font-size:18px;text-overflow:ellipsis;white-space:nowrap}.carton-detail-header span{color:#7c899c;font-size:12px}.carton-detail-actions{display:flex;align-items:center;gap:8px}.carton-detail-section+.carton-detail-section{border-top:1px solid var(--client-border)}.carton-detail-section>h3{display:flex;min-height:40px;align-items:center;margin:0;padding:0 16px;border-bottom:1px solid var(--client-border);background:var(--client-header-bg);font-size:14px}.carton-detail-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));margin:0}.carton-detail-grid div{display:grid;min-height:42px;grid-template-columns:88px minmax(0,1fr);align-items:center;gap:8px;padding:5px 12px;border-right:1px solid var(--client-border);border-bottom:1px solid var(--client-border)}.carton-detail-grid dt{color:var(--client-muted);font-size:12px}.carton-detail-grid dd{margin:0;overflow:hidden;color:var(--client-text);font-size:13px;font-weight:600;text-overflow:ellipsis;white-space:nowrap}@media(max-width:1100px){.carton-detail-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:720px){.carton-detail-header{grid-template-columns:1fr auto}.carton-detail-back{grid-column:1/-1}.carton-detail-grid{grid-template-columns:1fr}}
|
||||
.carton-detail-error{display:flex;min-height:220px;align-items:center;justify-content:center;gap:12px;color:#b42318}
|
||||
</style>
|
||||
|
||||
@@ -3,8 +3,18 @@ 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, /getMerchantWarehouses\(\{ page: 1, size: 200, warehouseType: 'OVERSEAS' \}\)/)
|
||||
assert.match(view, /@change="selectOperationWarehouse"/)
|
||||
assert.match(view, /warehouseContactName = warehouse\?\.contactName \|\| ''/)
|
||||
assert.match(view, /warehouseContactPhone = warehouse\?\.contactPhone \|\| ''/)
|
||||
@@ -17,9 +27,13 @@ test('新建退货预报选择海外仓后带出联系人和电话', () => {
|
||||
assert.match(view, /warehouseContactPhone: form\.warehouseContactPhone\.trim\(\)/)
|
||||
})
|
||||
|
||||
test('新建退货预报使用加宽的双列表单弹窗', () => {
|
||||
assert.match(view, /title="新建退货预报" width="1120px"/)
|
||||
assert.match(view, /<el-form label-width="190px" class="return-create-form">/)
|
||||
test('新建退货预报使用独立路由页面,编辑仍保留加宽弹框', () => {
|
||||
assert.match(view, /defineProps<\{ createPage\?: boolean; detailPage\?: boolean/)
|
||||
assert.match(view, /name: 'overseas-return-relabel-create'/)
|
||||
assert.match(view, /v-if="props\.createPage"/)
|
||||
assert.match(view, /class="return-create-surface"/)
|
||||
assert.match(view, /<h2>新建退货预报<\/h2>/)
|
||||
assert.match(view, /:title="editingReturnNo \? '编辑退货预报' : '新建退货预报'" width="1120px"/)
|
||||
assert.match(view, /return-create-form \{ display:grid; grid-template-columns:repeat\(2,minmax\(0,1fr\)\)/)
|
||||
})
|
||||
|
||||
@@ -28,25 +42,43 @@ 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, /<el-table-column prop="overseasWarehouseId"/)
|
||||
assert.doesNotMatch(view, /\{\{ selected\.order\.overseasWarehouseId \}\}/)
|
||||
})
|
||||
|
||||
test('可操作退货表格把操作列固定在最左侧', () => {
|
||||
const mainTable = view.slice(view.indexOf('<el-table v-loading="loading"'), view.indexOf('<el-pagination'))
|
||||
const createTable = view.slice(view.indexOf('<el-table :data="form.items"'), view.indexOf('</el-table>', view.indexOf('<el-table :data="form.items"')))
|
||||
assert.match(mainTable, /^<el-table[\s\S]*?<el-table-column label="操作" width="90" fixed="left">/)
|
||||
assert.match(view, /label: '操作', slotName: 'actions', width: 220, fixed: 'left'/)
|
||||
assert.match(view, /<DataTable[^>]*:row-actions="rowActions"/)
|
||||
assert.match(createTable, /^<el-table[\s\S]*?<el-table-column label="操作" width="80" fixed="left">/)
|
||||
})
|
||||
|
||||
test('分页初始化不会重复请求退货列表', () => {
|
||||
assert.match(view, /function changePage\(page: number\) \{\s*if \(page === query\.page\) return/)
|
||||
assert.match(view, /:current-page="query\.page" :page-size="query\.size"/)
|
||||
assert.match(view, /@current-change="changePage"/)
|
||||
assert.match(view, /watch\(\(\) => \[query\.page, query\.size\] as const/)
|
||||
assert.match(view, /v-model:page="query\.page" v-model:page-size="query\.size"/)
|
||||
assert.doesNotMatch(view, /@current-change="load"/)
|
||||
})
|
||||
|
||||
test('退货签收详情使用独立路由和紧凑连续页面', () => {
|
||||
assert.match(view, /name: 'overseas-return-relabel-detail'/)
|
||||
assert.match(view, /class="return-detail-surface"/)
|
||||
assert.match(view, /class="return-detail-grid"/)
|
||||
assert.match(view, /function returnToList\(\)/)
|
||||
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: '换标单状态'/)
|
||||
})
|
||||
|
||||
@@ -1,24 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Plus, Search } from '@lucide/vue'
|
||||
import { ArrowLeft, Plus } from '@lucide/vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
||||
import type { TableAction } from '@/components/common/tableActions'
|
||||
import { useAuthStore } from '@/modules/auth/public'
|
||||
import { getMerchantWarehouses, type MerchantWarehouse } from '@/modules/first-leg/public'
|
||||
import { createReturnOrder, getReturnOrder, listReturnOrders, type ReturnOrderDetail, type ReturnOrderSummary, type ReturnReceipt } from '../api/returnOrders'
|
||||
import { createReturnOrder, downloadReturnPhoto, getReturnOrder, listReturnOrders, updateReturnOrder, type ReturnOrderDetail, type ReturnOrderSummary, type ReturnReceipt } from '../api/returnOrders'
|
||||
|
||||
const props = withDefaults(defineProps<{ createPage?: boolean; detailPage?: boolean; returnNo?: string }>(), { createPage: false, detailPage: false, returnNo: '' })
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const detailReturnNo = computed(() => props.returnNo || String(route.params.returnNo || ''))
|
||||
const loading = ref(false)
|
||||
const auth = useAuthStore()
|
||||
const canCreate = computed(() => auth.hasPermission('merchant:overseas-warehouse:return-order:create'))
|
||||
const canCreateRelabel = computed(() => auth.hasPermission('merchant:overseas-warehouse:relabel:create'))
|
||||
const creating = ref(false)
|
||||
const showCreate = ref(false)
|
||||
const showDetail = ref(false)
|
||||
const editingReturnNo = ref('')
|
||||
const selected = ref<ReturnOrderDetail>()
|
||||
const photoUrls = reactive<Record<string, string>>({})
|
||||
const rows = ref<ReturnOrderSummary[]>([])
|
||||
const total = ref(0)
|
||||
const operationWarehouses = ref<MerchantWarehouse[]>([])
|
||||
const operationWarehousesLoading = ref(false)
|
||||
const operationWarehousesError = ref('')
|
||||
const query = reactive({ page: 1, size: 15, returnNo: '', trackingNo: '', skuCode: '', status: '' })
|
||||
const queryModel = computed<QueryModel>({ get: () => ({ ...query }), set: (value) => Object.assign(query, value) })
|
||||
const queryFields: QueryField[] = [
|
||||
{ key: 'returnNo', label: '退货单号', type: 'input', placeholder: '请输入退货单号' },
|
||||
{ key: 'trackingNo', label: '快递单号', type: 'input', placeholder: '请输入快递单号' },
|
||||
{ key: 'skuCode', label: 'SKU', type: 'input', placeholder: '请输入 SKU' },
|
||||
{ key: 'status', label: '签收状态', type: 'select', options: [
|
||||
{ label: '待审核', value: 'PENDING_REVIEW' },
|
||||
{ label: '待签收', value: 'PENDING_RECEIPT' },
|
||||
{ label: '部分签收', value: 'PARTIALLY_RECEIVED' },
|
||||
{ label: '已完成入库', value: 'COMPLETED' },
|
||||
{ label: '已驳回', value: 'REJECTED' },
|
||||
{ label: '已取消', value: 'CANCELLED' },
|
||||
] },
|
||||
]
|
||||
const tableColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', slotName: 'actions', width: 220, fixed: 'left' },
|
||||
{ prop: 'returnNo', label: '退货单号', minWidth: 190 }, { prop: 'customerReferenceNo', label: '客户参考号', minWidth: 160 },
|
||||
{ key: 'warehouse', label: '操作仓', slotName: 'warehouse', minWidth: 200 }, { key: 'relabel', label: '换标', slotName: 'relabel', width: 100 },
|
||||
{ prop: 'skuTypes', label: 'SKU 种类', width: 105 }, { prop: 'expectedQuantity', label: '预报数量', width: 110 },
|
||||
{ prop: 'receivedQuantity', label: '实收数量', width: 110 }, { key: 'status', label: '状态', slotName: 'status', width: 150 },
|
||||
{ prop: 'relabelNo', label: '换标单号', minWidth: 190 }, { key: 'relabelStatus', label: '换标单状态', slotName: 'relabelStatus', width: 130 },
|
||||
{ key: 'createdAt', label: '创建时间', slotName: 'createdAt', width: 180 },
|
||||
]
|
||||
const itemColumns: DataTableColumn[] = [
|
||||
{ prop: 'skuCode', label: 'SKU', minWidth: 190 },
|
||||
{ prop: 'productNameZh', label: '中文品名', minWidth: 220 },
|
||||
{ prop: 'expectedQuantity', label: '预报数量', width: 110 },
|
||||
{ prop: 'receivedQuantity', label: '实收数量', width: 110 },
|
||||
]
|
||||
const receiptColumns: DataTableColumn[] = [
|
||||
{ prop: 'receiptBatchNo', label: '批次号', minWidth: 170 },
|
||||
{ prop: 'receiptNo', label: '签收单号', minWidth: 170 },
|
||||
{ prop: 'palletCode', label: '托盘码', minWidth: 190 },
|
||||
{ key: 'trackingNo', label: '快递单号', slotName: 'trackingNo', minWidth: 160 },
|
||||
{ key: 'startedBy', label: '开始方式', slotName: 'startedBy', width: 110 },
|
||||
{ prop: 'receivedQuantity', label: '实收', width: 90 },
|
||||
{ key: 'signedAt', label: '签收时间', slotName: 'signedAt', minWidth: 180 },
|
||||
]
|
||||
interface SkuDraft { id: number; skuCode: string; productNameZh: string; expectedQuantity: number }
|
||||
let skuSequence = 0
|
||||
function emptySku(): SkuDraft { return { id: ++skuSequence, skuCode: '', productNameZh: '', expectedQuantity: 1 } }
|
||||
@@ -50,7 +98,7 @@ async function loadOperationWarehouses() {
|
||||
try {
|
||||
// Managed transit warehouses are maintained as OVERSEAS_PARTNER in the warehouse master.
|
||||
// Query the active catalogue and accept both overseas warehouse types.
|
||||
const result = await getMerchantWarehouses({ page: 1, size: 200 })
|
||||
const result = await getMerchantWarehouses({ page: 1, size: 200, warehouseType: 'OVERSEAS' })
|
||||
operationWarehouses.value = result.records.filter((warehouse) =>
|
||||
warehouse.status === 'ACTIVE' && ['OVERSEAS', 'OVERSEAS_PARTNER'].includes(warehouse.warehouseType),
|
||||
)
|
||||
@@ -62,9 +110,21 @@ async function loadOperationWarehouses() {
|
||||
} finally { operationWarehousesLoading.value = false }
|
||||
}
|
||||
function openCreate() {
|
||||
resetForm()
|
||||
void router.push({ name: 'overseas-return-relabel-create' })
|
||||
}
|
||||
async function openEdit(returnNo: string) {
|
||||
const detail = await getReturnOrder(returnNo)
|
||||
editingReturnNo.value = returnNo
|
||||
Object.assign(form, {
|
||||
overseasWarehouseId: String(detail.order.overseasWarehouseId),
|
||||
warehouseContactName: detail.order.warehouseContactName || '',
|
||||
warehouseContactPhone: detail.order.warehouseContactPhone || '',
|
||||
customerReferenceNo: detail.order.customerReferenceNo || '',
|
||||
remark: detail.remark || '',
|
||||
needRelabel: Boolean(detail.order.needRelabel),
|
||||
items: detail.items.map(item => ({ id: ++skuSequence, skuCode: item.skuCode, productNameZh: item.productNameZh || '', expectedQuantity: item.expectedQuantity })),
|
||||
})
|
||||
showCreate.value = true
|
||||
if (!operationWarehouses.value.length && !operationWarehousesLoading.value) void loadOperationWarehouses()
|
||||
}
|
||||
function addItem() { form.items.push(emptySku()) }
|
||||
function removeItem(index: number) { if (form.items.length > 1) form.items.splice(index, 1) }
|
||||
@@ -78,7 +138,46 @@ function changePage(page: number) {
|
||||
query.page = page
|
||||
void load()
|
||||
}
|
||||
async function openDetail(returnNo: string) { selected.value = await getReturnOrder(returnNo); showDetail.value = true }
|
||||
function search() { if (query.page === 1) void load(); else query.page = 1 }
|
||||
function rowActions(candidate: object): TableAction[] {
|
||||
const row = candidate as ReturnOrderSummary
|
||||
const actions: TableAction[] = [{ key: 'detail', label: '详情', run: () => openDetail(row.returnNo) }]
|
||||
if (canCreate.value && row.status === 'PENDING_REVIEW') actions.push({ key: 'edit', label: '编辑', run: () => openEdit(row.returnNo) })
|
||||
if (canCreateRelabel.value && canApplyRelabel(row)) actions.push({ key: 'relabel', label: '贴标申请', run: () => openRelabel(row) })
|
||||
return actions
|
||||
}
|
||||
async function loadDetail() {
|
||||
if (!detailReturnNo.value) return
|
||||
loading.value = true
|
||||
clearPhotoUrls()
|
||||
try {
|
||||
selected.value = await getReturnOrder(detailReturnNo.value)
|
||||
await Promise.all((selected.value.photos || []).map(async (photo) => {
|
||||
try {
|
||||
photoUrls[String(photo.fileId)] = URL.createObjectURL(await downloadReturnPhoto(detailReturnNo.value, photo.fileId))
|
||||
} catch {
|
||||
// 单张照片加载失败不阻断详情展示。
|
||||
}
|
||||
}))
|
||||
}
|
||||
catch (error) { ElMessage.error(error instanceof Error ? error.message : '退货签收详情加载失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function clearPhotoUrls() {
|
||||
Object.values(photoUrls).forEach(url => URL.revokeObjectURL(url))
|
||||
Object.keys(photoUrls).forEach(key => delete photoUrls[key])
|
||||
}
|
||||
function previewPhoto(fileId: string | number) {
|
||||
const url = photoUrls[String(fileId)]
|
||||
if (url) window.open(url, '_blank', 'noopener')
|
||||
}
|
||||
function openDetail(returnNo: string) { void router.push({ name: 'overseas-return-relabel-detail', params: { returnNo } }) }
|
||||
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<ReturnOrderSummary, 'returnNo' | 'overseasWarehouseId'>) {
|
||||
void router.push({ name: 'overseas-labeling-create', query: { returnNo: order.returnNo, warehouseId: String(order.overseasWarehouseId) } })
|
||||
}
|
||||
async function submit() {
|
||||
const items = form.items.map(({ skuCode, productNameZh, expectedQuantity }) => ({ skuCode: skuCode.trim(), productNameZh: productNameZh.trim(), expectedQuantity }))
|
||||
if (!form.overseasWarehouseId || items.some(item => !item.skuCode || item.expectedQuantity <= 0)) { ElMessage.warning('请填写操作仓、SKU 和预报数量'); return }
|
||||
@@ -86,13 +185,28 @@ async function submit() {
|
||||
if (duplicateSkuIds.value.size) { ElMessage.warning('同一退货单不能重复填写相同 SKU'); return }
|
||||
creating.value = true
|
||||
try {
|
||||
const detail = await createReturnOrder({ overseasWarehouseId: form.overseasWarehouseId, warehouseContactName: form.warehouseContactName.trim(), warehouseContactPhone: form.warehouseContactPhone.trim(), customerReferenceNo: form.customerReferenceNo || undefined, remark: form.remark || undefined, needRelabel: form.needRelabel, idempotencyKey: crypto.randomUUID(), items: items.map(item => ({ ...item, productNameZh: item.productNameZh || undefined })) })
|
||||
ElMessage.success('退货预报已创建'); showCreate.value = false; resetForm(); await load(); await openDetail(detail.order.returnNo)
|
||||
const payload = { overseasWarehouseId: form.overseasWarehouseId, warehouseContactName: form.warehouseContactName.trim(), warehouseContactPhone: form.warehouseContactPhone.trim(), customerReferenceNo: form.customerReferenceNo || undefined, remark: form.remark || undefined, needRelabel: form.needRelabel, idempotencyKey: crypto.randomUUID(), items: items.map(item => ({ ...item, productNameZh: item.productNameZh || undefined })) }
|
||||
const detail = editingReturnNo.value ? await updateReturnOrder(editingReturnNo.value, payload) : await createReturnOrder(payload)
|
||||
ElMessage.success(editingReturnNo.value ? '退货预报已更新' : '退货预报已创建'); showCreate.value = false; editingReturnNo.value = ''; resetForm(); await router.push({ name: 'overseas-return-relabel-detail', params: { returnNo: detail.order.returnNo } })
|
||||
} finally { creating.value = false }
|
||||
}
|
||||
function statusType(status: string) { return status === 'COMPLETED' ? 'success' : status === 'PARTIALLY_RECEIVED' ? 'warning' : 'info' }
|
||||
function statusLabel(status: string) {
|
||||
return ({ PENDING_RECEIPT: '待签收', PARTIALLY_RECEIVED: '部分签收', COMPLETED: '已完成' } as Record<string, string>)[status] ?? '未知状态'
|
||||
function statusType(status: string, needRelabel = false) {
|
||||
if (status === 'COMPLETED') return needRelabel ? 'warning' : 'success'
|
||||
return ({ PENDING_REVIEW: 'warning', PENDING_RECEIPT: 'primary', PARTIALLY_RECEIVED: 'warning', REJECTED: 'danger', CANCELLED: 'info' } as Record<string, 'primary' | 'success' | 'warning' | 'danger' | 'info'>)[status] || 'info'
|
||||
}
|
||||
function statusClass(status: string, needRelabel = false) {
|
||||
if (status === 'COMPLETED') return needRelabel ? 'return-status--relabel' : 'return-status--completed'
|
||||
return `return-status--${status.toLowerCase().replaceAll('_', '-')}`
|
||||
}
|
||||
function statusLabel(status: string, needRelabel = false) {
|
||||
if (status === 'COMPLETED') return needRelabel ? '已完成入库/待换标' : '已完成入库'
|
||||
return ({ PENDING_REVIEW: '待审核', PENDING_RECEIPT: '待签收', PARTIALLY_RECEIVED: '部分签收', REJECTED: '已驳回', CANCELLED: '已取消' } as Record<string, string>)[status] ?? (status || '未知状态')
|
||||
}
|
||||
function relabelStatusType(status?: string) {
|
||||
return ({ PENDING_REVIEW: 'warning', PENDING_PROCESSING: 'primary', PROCESSING: 'primary', CUSTOMER_CONFIRM_PENDING: 'warning', COMPLETED: 'success', REJECTED: 'danger', CANCELLED: 'info' } as Record<string, 'primary' | 'success' | 'warning' | 'danger' | 'info'>)[status || ''] || 'info'
|
||||
}
|
||||
function relabelStatusLabel(status?: string) {
|
||||
return ({ PENDING_REVIEW: '待审核', PENDING_PROCESSING: '待处理', PROCESSING: '处理中', CUSTOMER_CONFIRM_PENDING: '待客户确认', COMPLETED: '已完成', REJECTED: '已驳回', CANCELLED: '已取消' } as Record<string, string>)[status || ''] ?? (status || '—')
|
||||
}
|
||||
function startedByLabel(startedBy: ReturnReceipt['startedBy']) { return startedBy === 'SKU' ? 'SKU 查询' : '退货单号' }
|
||||
function formatDateTime(value?: string | null) {
|
||||
@@ -101,34 +215,62 @@ function formatDateTime(value?: string | null) {
|
||||
if (Number.isNaN(date.getTime())) return '—'
|
||||
return new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).format(date)
|
||||
}
|
||||
onMounted(() => { void Promise.all([load(), loadOperationWarehouses()]) })
|
||||
onMounted(() => {
|
||||
if (props.createPage) { resetForm(); void loadOperationWarehouses() }
|
||||
else if (props.detailPage) void Promise.all([loadDetail(), loadOperationWarehouses()])
|
||||
else void Promise.all([load(), loadOperationWarehouses()])
|
||||
})
|
||||
watch(() => [query.page, query.size] as const, ([nextPage, nextSize], [previousPage, previousSize]) => {
|
||||
if (nextSize !== previousSize && nextPage !== 1) { query.page = 1; return }
|
||||
if (!props.createPage && !props.detailPage) void load()
|
||||
})
|
||||
onBeforeUnmount(clearPhotoUrls)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="return-orders-page">
|
||||
<div class="page-heading"><div><h2>退货签收</h2><p>预报退货 SKU 和数量,仓库 PDA 实收后自动更新签收状态。</p></div><el-button v-if="canCreate" type="primary" @click="openCreate"><Plus :size="16" />新建退货预报</el-button></div>
|
||||
<div class="filter-bar">
|
||||
<el-input v-model="query.returnNo" clearable placeholder="退货单号" @keyup.enter="query.page=1; load()" />
|
||||
<el-input v-model="query.trackingNo" clearable placeholder="快递单号" @keyup.enter="query.page=1; load()" />
|
||||
<el-input v-model="query.skuCode" clearable placeholder="SKU" @keyup.enter="query.page=1; load()" />
|
||||
<el-select v-model="query.status" clearable placeholder="签收状态"><el-option label="待签收" value="PENDING_RECEIPT" /><el-option label="部分签收" value="PARTIALLY_RECEIVED" /><el-option label="已完成" value="COMPLETED" /></el-select>
|
||||
<el-button @click="query.page=1; load()"><Search :size="16" />查询</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" class="return-table-card" :data="rows" @row-click="row => openDetail(row.returnNo)">
|
||||
<el-table-column label="操作" width="90" fixed="left"><template #default="{ row }"><el-button link type="primary" @click.stop="openDetail(row.returnNo)">详情</el-button></template></el-table-column>
|
||||
<el-table-column prop="returnNo" label="退货单号" min-width="190" />
|
||||
<el-table-column prop="customerReferenceNo" label="客户参考号" min-width="150" />
|
||||
<el-table-column label="操作仓" min-width="190"><template #default="{ row }">{{ operationWarehouseLabel(row.overseasWarehouseId) }}</template></el-table-column>
|
||||
<el-table-column label="换标" width="100"><template #default="{ row }"><el-tag v-if="row.needRelabel" type="warning">需要换标</el-tag><span v-else>否</span></template></el-table-column>
|
||||
<el-table-column prop="skuTypes" label="SKU种类" width="100" />
|
||||
<el-table-column prop="expectedQuantity" label="预报数量" width="110" />
|
||||
<el-table-column prop="receivedQuantity" label="实收数量" width="110" />
|
||||
<el-table-column label="状态" width="130"><template #default="{ row }"><el-tag :type="statusType(row.status)">{{ statusLabel(row.status) }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="创建时间" min-width="180"><template #default="{ row }">{{ formatDateTime(row.createdAt) }}</template></el-table-column>
|
||||
</el-table>
|
||||
<div class="return-pagination"><span class="return-pagination-total">共 {{ total }} 条</span><el-select v-model="query.size" class="return-page-size-select" aria-label="每页条数" @change="query.page=1; load()"><el-option v-for="size in [15, 30, 50, 100]" :key="size" :label="`${size}条/页`" :value="size" /></el-select><el-pagination :current-page="query.page" :page-size="query.size" :total="total" :hide-on-single-page="false" layout="prev, pager, next" @current-change="changePage" /></div>
|
||||
<template v-if="props.createPage">
|
||||
<article class="return-create-surface">
|
||||
<header class="return-create-header">
|
||||
<div><h2>新建退货预报</h2><p>填写预计退回的 SKU 和数量,仓库签收后将自动更新实收结果。</p></div>
|
||||
<el-button @click="returnToList"><ArrowLeft :size="16" />返回列表</el-button>
|
||||
</header>
|
||||
<div class="return-create-body">
|
||||
<el-form label-width="150px" class="return-create-form">
|
||||
<el-form-item label="海外仓" required class="return-form-wide"><el-select v-model="form.overseasWarehouseId" filterable clearable :loading="operationWarehousesLoading" :disabled="Boolean(operationWarehousesError)" placeholder="请选择海外仓" @change="selectOperationWarehouse"><el-option v-for="warehouse in operationWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="String(warehouse.id)" /></el-select><div v-if="operationWarehousesError" class="warehouse-error">{{ operationWarehousesError }}</div></el-form-item>
|
||||
<el-form-item label="海外仓联系人" required><el-input v-model="form.warehouseContactName" :readonly="Boolean(selectedOperationWarehouse?.contactName)" :placeholder="selectedOperationWarehouse?.contactName ? '已从海外仓资料带出' : '该海外仓未维护,请手动填写'" /></el-form-item>
|
||||
<el-form-item label="海外仓联系电话" required><el-input v-model="form.warehouseContactPhone" :readonly="Boolean(selectedOperationWarehouse?.contactPhone)" :placeholder="selectedOperationWarehouse?.contactPhone ? '已从海外仓资料带出' : '该海外仓未维护,请手动填写'" /></el-form-item>
|
||||
<el-form-item label="客户参考号" class="return-form-wide"><el-input v-model="form.customerReferenceNo" placeholder="可填写客户侧参考号" /></el-form-item>
|
||||
<el-form-item label="备注" class="return-form-wide"><el-input v-model="form.remark" type="textarea" :rows="3" placeholder="填写退货说明(选填)" /></el-form-item>
|
||||
</el-form>
|
||||
<div class="items-title"><span>SKU 预报明细</span><el-button text type="primary" @click="addItem"><Plus :size="15" />添加 SKU</el-button></div>
|
||||
<div class="return-relabel-option"><span>换标</span><el-checkbox v-model="form.needRelabel">需要换标</el-checkbox></div>
|
||||
<div class="items-hint">客户只需填写预计退回的 SKU 和数量;快递单号由仓库签收时按实际包裹扫描绑定。</div>
|
||||
<el-table :data="form.items" row-key="id" size="small" border>
|
||||
<el-table-column label="操作" width="80" fixed="left"><template #default="{ $index }"><el-button link type="danger" :disabled="form.items.length === 1" @click="removeItem($index)">删除</el-button></template></el-table-column>
|
||||
<el-table-column label="SKU" min-width="240"><template #default="{ row }"><div><el-input v-model="row.skuCode" :class="{ 'duplicate-sku-input': duplicateSkuIds.has(row.id) }" placeholder="填写 SKU" @blur="row.skuCode = row.skuCode.trim()" /><div v-if="duplicateSkuIds.has(row.id)" class="duplicate-sku-error">该 SKU 已在明细中填写</div></div></template></el-table-column>
|
||||
<el-table-column label="中文品名" min-width="320"><template #default="{ row }"><el-input v-model="row.productNameZh" placeholder="填写中文品名(选填)" /></template></el-table-column>
|
||||
<el-table-column label="预报数量" width="180"><template #default="{ row }"><el-input-number v-model="row.expectedQuantity" :min="1" controls-position="right" placeholder="数量" /></template></el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<footer class="return-create-actions"><el-button @click="cancelCreate">取消</el-button><el-button v-if="canCreate" type="primary" :loading="creating" @click="submit">提交预报</el-button></footer>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<el-dialog v-model="showCreate" title="新建退货预报" width="1120px" destroy-on-close>
|
||||
<template v-else-if="!props.detailPage">
|
||||
<QueryToolbar v-model="queryModel" layout="grid" ariaLabel="退货签收筛选" :fields="queryFields" @search="search" />
|
||||
<div class="return-table-card">
|
||||
<DataTable v-model:page="query.page" v-model:page-size="query.size" :data="rows" :columns="tableColumns" :loading="loading" :total="total" :page-sizes="[15, 30, 50, 100]" :row-actions="rowActions" row-key="returnNo" empty-text="暂无退货预报">
|
||||
<template #toolbar-actions><el-button v-if="canCreate" type="primary" @click="openCreate"><Plus :size="16" />新建退货预报</el-button></template>
|
||||
<template #warehouse="{ row }">{{ operationWarehouseLabel(row.overseasWarehouseId) }}</template>
|
||||
<template #relabel="{ row }"><el-tag v-if="row.needRelabel" type="warning" effect="light">需要换标</el-tag><span v-else>否</span></template>
|
||||
<template #status="{ row }"><el-tag :type="statusType(row.status, row.needRelabel)" :class="statusClass(row.status, row.needRelabel)" effect="light">{{ statusLabel(row.status, row.needRelabel) }}</el-tag></template>
|
||||
<template #relabelStatus="{ row }"><el-tag v-if="row.relabelStatus" :type="relabelStatusType(row.relabelStatus)" effect="light">{{ relabelStatusLabel(row.relabelStatus) }}</el-tag><span v-else>—</span></template>
|
||||
<template #createdAt="{ row }">{{ formatDateTime(row.createdAt) }}</template>
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="showCreate" :title="editingReturnNo ? '编辑退货预报' : '新建退货预报'" width="1120px" destroy-on-close>
|
||||
<el-form label-width="190px" class="return-create-form">
|
||||
<el-form-item label="海外仓" required class="return-form-wide"><el-select v-model="form.overseasWarehouseId" filterable clearable :loading="operationWarehousesLoading" :disabled="Boolean(operationWarehousesError)" placeholder="请选择海外仓" @change="selectOperationWarehouse"><el-option v-for="warehouse in operationWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="String(warehouse.id)" /></el-select><div v-if="operationWarehousesError" class="warehouse-error">{{ operationWarehousesError }}</div></el-form-item>
|
||||
<el-form-item label="海外仓联系人" required><el-input v-model="form.warehouseContactName" :readonly="Boolean(selectedOperationWarehouse?.contactName)" :placeholder="selectedOperationWarehouse?.contactName ? '已从海外仓资料带出' : '该海外仓未维护,请手动填写'" /></el-form-item>
|
||||
@@ -145,36 +287,122 @@ onMounted(() => { void Promise.all([load(), loadOperationWarehouses()]) })
|
||||
<el-table-column label="中文品名" min-width="260"><template #default="{ row }"><el-input v-model="row.productNameZh" placeholder="填写中文品名(选填)" /></template></el-table-column>
|
||||
<el-table-column label="预报数量" width="160"><template #default="{ row }"><el-input-number v-model="row.expectedQuantity" :min="1" controls-position="right" placeholder="数量" /></template></el-table-column>
|
||||
</el-table>
|
||||
<template #footer><el-button @click="showCreate=false">取消</el-button><el-button v-if="canCreate" type="primary" :loading="creating" @click="submit">提交预报</el-button></template>
|
||||
<template #footer><el-button @click="showCreate=false; editingReturnNo=''">取消</el-button><el-button v-if="canCreate" type="primary" :loading="creating" @click="submit">{{ editingReturnNo ? '保存修改' : '提交预报' }}</el-button></template>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="showDetail" title="退货签收详情" width="min(980px, calc(100vw - 32px))" top="8vh" class="return-detail-dialog" destroy-on-close>
|
||||
</template>
|
||||
|
||||
<article v-else v-loading="loading" class="return-detail-surface">
|
||||
<template v-if="selected">
|
||||
<div class="detail-summary"><div><span>退货单号</span><strong>{{ selected.order.returnNo }}</strong></div><div><span>状态</span><el-tag :type="statusType(selected.order.status)">{{ statusLabel(selected.order.status) }}</el-tag></div><div><span>海外仓</span><strong>{{ operationWarehouseLabel(selected.order.overseasWarehouseId) }}</strong></div><div><span>实收 / 预报</span><strong>{{ selected.order.receivedQuantity }} / {{ selected.order.expectedQuantity }}</strong></div><div><span>海外仓联系人</span><strong>{{ selected.order.warehouseContactName || '—' }}</strong></div><div><span>联系电话</span><strong>{{ selected.order.warehouseContactPhone || '—' }}</strong></div></div>
|
||||
<p v-if="selected.remark" class="remark detail-remark">备注:{{ selected.remark }}</p>
|
||||
<section class="detail-section"><h3>预报与实收</h3><el-table :data="selected.items" size="small" border><el-table-column prop="skuCode" label="SKU" min-width="180" /><el-table-column prop="productNameZh" label="中文品名" min-width="220" /><el-table-column prop="expectedQuantity" label="预报" width="100" /><el-table-column prop="receivedQuantity" label="实收" width="100" /></el-table></section>
|
||||
<section class="detail-section"><h3>签收批次</h3><el-table :data="selected.receipts" size="small" border><el-table-column prop="receiptBatchNo" label="批次号" min-width="160" /><el-table-column prop="receiptNo" label="签收单号" min-width="160" /><el-table-column prop="cartonMark" label="箱号" min-width="170" /><el-table-column prop="trackingNo" label="快递单号" min-width="150"><template #default="{ row }">{{ row.trackingNo || '无快递单号' }}</template></el-table-column><el-table-column label="开始方式" width="110"><template #default="{ row }">{{ startedByLabel(row.startedBy) }}</template></el-table-column><el-table-column prop="receivedQuantity" label="实收" width="80" /><el-table-column label="签收时间" min-width="170"><template #default="{ row }">{{ formatDateTime(row.signedAt) }}</template></el-table-column></el-table></section>
|
||||
<header class="return-detail-header">
|
||||
<button type="button" class="return-back" @click="returnToList"><ArrowLeft :size="16" />返回列表</button>
|
||||
<div class="return-detail-title"><strong>{{ selected.order.returnNo }}</strong><span>{{ operationWarehouseLabel(selected.order.overseasWarehouseId) }} · 退货签收</span></div>
|
||||
<div class="return-detail-actions"><el-button v-if="canCreateRelabel && canApplyRelabel(selected.order)" type="primary" plain @click="openRelabel(selected.order)">贴标申请</el-button><el-tag :type="statusType(selected.order.status, selected.order.needRelabel)" :class="statusClass(selected.order.status, selected.order.needRelabel)" effect="light">{{ statusLabel(selected.order.status, selected.order.needRelabel) }}</el-tag></div>
|
||||
</header>
|
||||
<section class="return-detail-section">
|
||||
<h3>签收概览</h3>
|
||||
<div class="return-detail-grid">
|
||||
<div><span>实收 / 预报</span><strong>{{ selected.order.receivedQuantity }} / {{ selected.order.expectedQuantity }}</strong></div>
|
||||
<div><span>SKU 种类</span><strong>{{ selected.order.skuTypes }}</strong></div>
|
||||
<div><span>是否换标</span><strong>{{ selected.order.needRelabel ? '需要换标' : '否' }}</strong></div>
|
||||
<div><span>换标单号</span><strong>{{ selected.order.relabelNo || '—' }}</strong></div>
|
||||
<div><span>换标单状态</span><strong>{{ relabelStatusLabel(selected.order.relabelStatus) }}</strong></div>
|
||||
<div><span>客户参考号</span><strong>{{ selected.order.customerReferenceNo || '—' }}</strong></div>
|
||||
<div><span>海外仓联系人</span><strong>{{ selected.order.warehouseContactName || '—' }}</strong></div>
|
||||
<div><span>联系电话</span><strong>{{ selected.order.warehouseContactPhone || '—' }}</strong></div>
|
||||
<div><span>创建时间</span><strong>{{ formatDateTime(selected.order.createdAt) }}</strong></div>
|
||||
</div>
|
||||
<div v-if="selected.remark" class="return-detail-remark"><span>备注</span><strong>{{ selected.remark }}</strong></div>
|
||||
</section>
|
||||
<section class="return-detail-section">
|
||||
<h3>预报与实收</h3>
|
||||
<DataTable :data="selected.items" :columns="itemColumns" row-key="skuCode" empty-text="暂无 SKU 明细" />
|
||||
</section>
|
||||
<section class="return-detail-section">
|
||||
<h3>打托明细({{ selected.pallets?.length || 0 }} 托)</h3>
|
||||
<el-collapse v-if="selected.pallets?.length" class="return-pallet-list">
|
||||
<el-collapse-item v-for="pallet in selected.pallets" :key="pallet.palletCode" :name="pallet.palletCode">
|
||||
<template #title><strong>{{ pallet.palletCode }}</strong><span class="return-pallet-summary">{{ pallet.skuTypes }} 种 SKU · {{ pallet.totalQuantity }} 件</span></template>
|
||||
<el-table :data="pallet.items" size="small" border>
|
||||
<el-table-column prop="skuCode" label="SKU" min-width="180" />
|
||||
<el-table-column prop="productNameZh" label="中文品名" min-width="220"><template #default="{ row }">{{ row.productNameZh || '—' }}</template></el-table-column>
|
||||
<el-table-column prop="quantity" label="数量" width="100" />
|
||||
</el-table>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
<el-empty v-else description="暂无打托记录" :image-size="64" />
|
||||
</section>
|
||||
<section class="return-detail-section">
|
||||
<h3>退货打托照片({{ selected.photos?.length || 0 }} 张)</h3>
|
||||
<div v-if="selected.photos?.length" class="return-photo-grid">
|
||||
<button v-for="photo in selected.photos" :key="photo.fileId" class="return-photo" type="button" @click="previewPhoto(photo.fileId)">
|
||||
<img v-if="photoUrls[String(photo.fileId)]" :src="photoUrls[String(photo.fileId)]" :alt="photo.fileName">
|
||||
<span v-else>照片加载失败</span>
|
||||
<small>{{ photo.fileName }}</small>
|
||||
</button>
|
||||
</div>
|
||||
<el-empty v-else description="暂无退货打托照片" :image-size="64" />
|
||||
</section>
|
||||
<section class="return-detail-section">
|
||||
<h3>签收批次</h3>
|
||||
<DataTable :data="selected.receipts" :columns="receiptColumns" row-key="receiptNo" empty-text="暂无签收批次">
|
||||
<template #trackingNo="{ row }">{{ row.trackingNo || '无快递单号' }}</template>
|
||||
<template #startedBy="{ row }">{{ startedByLabel(row.startedBy) }}</template>
|
||||
<template #signedAt="{ row }">{{ formatDateTime(row.signedAt) }}</template>
|
||||
</DataTable>
|
||||
</section>
|
||||
</template>
|
||||
<template #footer><el-button type="primary" @click="showDetail = false">关闭</el-button></template>
|
||||
</el-dialog>
|
||||
</article>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.return-orders-page { padding: 20px; } .page-heading { display:flex; justify-content:space-between; align-items:flex-start; gap:16px; margin-bottom:18px; } h2 { margin:0; font-size:22px; } p { margin:6px 0 0; color:var(--el-text-color-secondary); } .filter-bar { display:grid; grid-template-columns:repeat(4,minmax(120px,1fr)) auto; gap:10px; margin-bottom:14px; } .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; } .el-pagination { margin-top:16px; justify-content:flex-end; } 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 #e7ebf1; border-radius:4px; background:#fff; box-shadow:0 1px 3px rgba(25,42,70,.04); }
|
||||
.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-create-surface { overflow:hidden; border:1px solid var(--client-border); border-radius:var(--client-radius); background:#fff; }
|
||||
.return-create-header { display:flex; min-height:68px; align-items:center; justify-content:space-between; gap:20px; padding:12px 20px; border-bottom:1px solid var(--client-border); }
|
||||
.return-create-header h2 { margin:0; color:#1f2d45; font-size:20px; }
|
||||
.return-create-header p { margin:4px 0 0; color:#7c899c; font-size:12px; }
|
||||
.return-create-body { padding:18px 20px 14px; }
|
||||
.return-create-body :deep(.el-select), .return-create-body :deep(.el-input-number) { width:100%; }
|
||||
.return-create-body .items-title { margin-top:8px; }
|
||||
.return-relabel-option { display:flex; align-items:center; gap:16px; margin:8px 0; color:#526176; }
|
||||
.return-create-actions { display:flex; align-items:center; justify-content:flex-end; gap:10px; padding:12px 20px; border-top:1px solid var(--client-border); background:#fbfcfe; }
|
||||
.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; }
|
||||
.return-page-size-select { width:112px; margin-left:8px; }
|
||||
.return-page-size-select :deep(.el-input__inner), .return-page-size-select :deep(.el-select__selected-item) { color:#526176 !important; opacity:1 !important; -webkit-text-fill-color:#526176; }
|
||||
.detail-summary { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:18px 24px; padding:18px; border:1px solid #e5ebf3; border-radius:6px; background:#f8faff; }
|
||||
.detail-summary div { display:flex; min-width:0; flex-direction:column; gap:6px; }
|
||||
.detail-summary span { color:#7c899c; font-size:12px; }
|
||||
.detail-summary strong { overflow-wrap:anywhere; color:#26364f; font-size:14px; }
|
||||
.detail-remark { margin:14px 0 0; padding:10px 12px; border-left:3px solid #7aa7dc; background:#f7faff; color:#526176; }
|
||||
.detail-section h3 { margin:22px 0 10px; color:#26364f; font-size:15px; }
|
||||
.return-detail-dialog :deep(.el-dialog__body) { padding-top:4px; }
|
||||
@media (max-width: 760px) { .return-orders-page { padding:12px; } .page-heading { align-items:stretch; flex-direction:column; } .filter-bar { grid-template-columns:1fr 1fr; } }
|
||||
@media (max-width: 720px) { .detail-summary { grid-template-columns:repeat(2,minmax(0,1fr)); } .return-pagination { padding:12px; } }
|
||||
.return-detail-surface { overflow:hidden; min-height:240px; border:1px solid var(--client-border); border-radius:var(--client-radius); background:#fff; }
|
||||
.return-detail-header { display:grid; grid-template-columns:auto minmax(0,1fr) auto; align-items:center; gap:14px; min-height:64px; padding:10px 16px; border-bottom:1px solid var(--client-border); }
|
||||
.return-back { display:inline-flex; align-items:center; gap:5px; padding:5px 0; border:0; background:transparent; color:var(--el-color-primary); cursor:pointer; }
|
||||
.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; }
|
||||
.return-detail-grid div { display:flex; min-width:0; min-height:48px; align-items:center; gap:10px; padding:8px 14px; border-right:1px solid #edf1f5; border-bottom:1px solid #edf1f5; }
|
||||
.return-detail-grid span,.return-detail-remark span { flex:0 0 92px; color:#8490a1; font-size:12px; }
|
||||
.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: 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)); } }
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ArrowLeft, Download, Eye, Plus, Printer, Search, Trash2, Upload, X } from '@lucide/vue'
|
||||
import SingleLineText from '@/components/common/SingleLineText.vue'
|
||||
import { ArrowLeft, Download, Plus, Printer, Trash2, Upload, X } from '@lucide/vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
||||
import type { TableAction } from '@/components/common/tableActions'
|
||||
import { useAuthStore } from '@/modules/auth/public'
|
||||
import {
|
||||
createDiscard, createRelabel, createTransfer, getDiscard, getRelabel, getTransfer,
|
||||
@@ -22,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'
|
||||
@@ -30,11 +34,15 @@ 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 }
|
||||
|
||||
const props = defineProps<{ service: ServiceType }>()
|
||||
const props = withDefaults(defineProps<{ service: ServiceType; createPage?: boolean; detailPage?: boolean; detailNo?: string }>(), { createPage: false, detailPage: false, detailNo: '' })
|
||||
const auth = useAuthStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const detailNumber = computed(() => props.detailNo || String(route.params.detailNo || ''))
|
||||
const canCreate = computed(() => auth.hasPermission(({
|
||||
labeling: 'merchant:overseas-warehouse:relabel:create',
|
||||
transfer: 'merchant:overseas-warehouse:transfer:create',
|
||||
@@ -52,6 +60,15 @@ const configs: Record<ServiceType, ServiceConfig> = {
|
||||
relabel: { title: '退货换标', createTitle: '新建退货换标单', numberLabel: '退货换标单号', emptyText: '暂无退货换标订单', columns: ['操作', '退货换标单号', '来源 FBA 仓', '目的 FBA 仓', 'SKU 汇总', '退件总数', '是否换标', '换标状态', '尾程状态', '创建时间'] },
|
||||
}
|
||||
const config = computed(() => configs[props.service])
|
||||
const listQueryModel = computed<QueryModel>({
|
||||
get: () => ({ ...filters, createdDates: [filters.start, filters.end].filter(Boolean) }),
|
||||
set: (value) => Object.assign(filters, {
|
||||
number: String(value.number || ''), inboundNo: String(value.inboundNo || ''), warehouse: String(value.warehouse || ''),
|
||||
destination: String(value.destination || ''), status: String(value.status || ''),
|
||||
start: Array.isArray(value.createdDates) ? String(value.createdDates[0] || '') : '',
|
||||
end: Array.isArray(value.createdDates) ? String(value.createdDates[1] || '') : '',
|
||||
}),
|
||||
})
|
||||
const statusOptions = computed(() => props.service === 'labeling'
|
||||
? [{ label: '待处理', value: 'PENDING_PROCESSING' }, { label: '处理中', value: 'PROCESSING' }, { label: '换标完成', value: 'COMPLETED' }]
|
||||
: props.service === 'disposal'
|
||||
@@ -65,7 +82,7 @@ const statusOptions = computed(() => props.service === 'labeling'
|
||||
{ label: '部分收货', value: 'PARTIALLY_RECEIVED' }, { label: '已收货', value: 'RECEIVED' },
|
||||
{ label: '审核驳回', value: 'REJECTED' }]
|
||||
: [{ label: '待审核', value: 'PENDING_REVIEW' }, { label: '已通过', value: 'APPROVED' }, { label: '待分配', value: 'PENDING_ASSIGNMENT' }, { label: '已分配', value: 'ALLOCATED' }, { label: '已出库', value: 'OUTBOUNDED' }, { label: '部分收货', value: 'PARTIALLY_RECEIVED' }, { label: '已收货', value: 'RECEIVED' }, { label: '已拒绝', value: 'REJECTED' }])
|
||||
const mode = ref<PageMode>('list')
|
||||
const mode = ref<PageMode>(props.detailPage ? 'detail' : props.createPage ? 'create' : 'list')
|
||||
const filters = reactive({ number: '', inboundNo: '', warehouse: '', destination: '', status: '', start: '', end: '' })
|
||||
const form = reactive({ customerNo: '', inboundNo: '', sourceWarehouse: '', warehouse: '', destination: '', privateDestinationAddress: '', privateDestinationPostalCode: '', privateDestinationContact: '', privateDestinationPhone: '', address: '', contact: '', phone: '', pickupContact: '', pickupPhone: '', relabelContact: '', relabelPhone: '', deliveryMethod: '', relabelDeliveryMethod: '', trackingNo: '', reason: '', requirement: '', relabel: false, remark: '', labelDeadline: '', estimatedArrivalAt: '', estimatedShipAt: '', fbaShipmentNo: '', volumeCbm: 0.01 })
|
||||
const transferDestinationType = ref<'NONE' | 'FBA' | 'PRIVATE'>('NONE')
|
||||
@@ -85,11 +102,6 @@ const shippingLabelFile = ref<File | null>(null)
|
||||
const skuRelabelRows = ref<SkuRelabelRow[]>([])
|
||||
const importedRelabelBindings = ref<ImportedRelabelBinding[]>([])
|
||||
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<Record<string, string>[]>([])
|
||||
const page = ref(1)
|
||||
const pageSize = ref(15)
|
||||
@@ -106,12 +118,12 @@ interface ServiceDetail extends Record<string, unknown> {
|
||||
estimatedShipAt?: string; actualShipAt?: string; fbaShipmentNo?: string; trackingNo?: string
|
||||
remark?: string; needRelabel?: boolean; labelDeadline?: string; relabelContactName?: string
|
||||
relabelContactPhone?: string; relabelDeliveryMethod?: string
|
||||
createdAt?: string
|
||||
items?: Array<{ skuCode?: string; productNameZh?: string; packageCount?: number; shippingMark?: string }>
|
||||
relabelBindings?: Array<{ operationType?: string; customerCartonMark?: string; originalSku?: string; newSku?: string; quantity?: number; fbaCartonMark?: string; destinationWarehouseCode?: string; remark?: string }>
|
||||
cartons?: TransferCarton[]; pallets?: TransferPallet[]; measurementPhotos?: TransferMeasurementPhoto[]; customerConfirmedAt?: string
|
||||
}
|
||||
const selectedDetail = ref<ServiceDetail | null>(null)
|
||||
const detailError = ref('')
|
||||
const selectedRelabelDetail = ref<RelabelDetail | null>(null)
|
||||
const completionPhotoUrls = reactive<Record<string, string>>({})
|
||||
const completionPhotoLoading = reactive<Record<string, boolean>>({})
|
||||
@@ -128,23 +140,52 @@ const operationWarehouses = ref<MerchantWarehouse[]>([])
|
||||
const fbaWarehousesLoading = ref(false)
|
||||
const fbaWarehousesError = ref('')
|
||||
const operationWarehousesError = ref('')
|
||||
const listQueryFields = computed<QueryField[]>(() => {
|
||||
const fields: QueryField[] = [
|
||||
{ key: 'number', label: config.value.numberLabel, type: 'input', placeholder: `请输入${config.value.numberLabel}` },
|
||||
]
|
||||
if (props.service !== 'labeling' && props.service !== 'relabel') fields.push({ key: 'inboundNo', label: '进仓编号', type: 'input', placeholder: '请输入进仓编号' })
|
||||
fields.push({
|
||||
key: 'warehouse', label: props.service === 'relabel' ? '来源 FBA 仓' : '操作仓', type: 'select',
|
||||
options: (props.service === 'relabel' ? fbaWarehouses.value : operationWarehouses.value).map((warehouse) => ({ label: warehouseLabel(warehouse), value: String(warehouse.id) })),
|
||||
})
|
||||
if (props.service === 'transfer' || props.service === 'relabel') fields.push({ key: 'destination', label: '目的 FBA 仓', type: 'input', placeholder: '请输入仓代码' })
|
||||
fields.push(
|
||||
{ key: 'status', label: '订单状态', type: 'select', options: statusOptions.value },
|
||||
{ key: 'createdDates', label: '创建时间', type: 'daterange' },
|
||||
)
|
||||
return fields
|
||||
})
|
||||
const listColumns = computed<DataTableColumn[]>(() => config.value.columns.map((label, index) => index === 0
|
||||
? { key: 'actions', label, slotName: 'actions', width: props.service === 'transfer' ? 210 : 160, fixed: 'left' }
|
||||
: { key: String(index - 1), prop: String(index - 1), label, minWidth: index === 1 ? 190 : 130 }))
|
||||
const discardCandidates = ref<import('../api/warehouseServices').DiscardCandidate[]>([])
|
||||
const selectedDiscardOrderId = ref('')
|
||||
const discardCandidatesLoading = ref(false)
|
||||
const relabelCandidates = ref<RelabelInventoryCandidate[]>([])
|
||||
const selectedRelabelInboundOrderId = ref('')
|
||||
const relabelCandidatesLoading = ref(false)
|
||||
const returnRelabelSkuRows = ref<ReturnRelabelSkuRow[]>([])
|
||||
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<string, { value: string; inboundOrderNo: string; inboundCode: string; availablePackages: number }>()
|
||||
const grouped = new Map<string, { value: string; inboundOrderNo: string; inboundCode: string; availablePackages: number; quantityUnit: '箱' | '件' }>()
|
||||
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()]
|
||||
@@ -165,7 +206,15 @@ const skuTotalCreateInput = ref<HTMLInputElement | null>(null)
|
||||
let cargoSequence = 0
|
||||
let skuRelabelSequence = 0
|
||||
|
||||
watch(() => props.service, () => { mode.value = 'list'; page.value = 1; resetFilters(); resetForm(); void loadPage() })
|
||||
watch(() => [props.service, props.createPage, props.detailPage, detailNumber.value] as const, () => {
|
||||
mode.value = props.detailPage ? 'detail' : props.createPage ? 'create' : 'list'
|
||||
page.value = 1
|
||||
resetFilters()
|
||||
resetForm()
|
||||
if (props.createPage) void openCreate()
|
||||
else if (props.detailPage && detailNumber.value) void loadDetailPage()
|
||||
else void loadPage()
|
||||
})
|
||||
watch(mode, (value) => { if (value !== 'detail') { clearCompletionPhotos(); clearMeasurementPhotos() } })
|
||||
watch(() => form.relabel, (value) => {
|
||||
if (props.service === 'transfer' && value) {
|
||||
@@ -194,7 +243,12 @@ async function loadPage() {
|
||||
if (props.service === 'labeling' || props.service === 'relabel' || props.service === 'transfer' || props.service === 'disposal') await loadWarehouses()
|
||||
await load()
|
||||
}
|
||||
onMounted(() => { void loadPage(); void nextTick(refreshWarehouseMarkLabels) })
|
||||
onMounted(() => {
|
||||
if (props.createPage) void openCreate()
|
||||
else if (props.detailPage && detailNumber.value) void loadDetailPage()
|
||||
else void loadPage()
|
||||
void nextTick(refreshWarehouseMarkLabels)
|
||||
})
|
||||
onBeforeUnmount(() => { clearCompletionPhotos(); clearMeasurementPhotos() })
|
||||
function resetFilters() { Object.assign(filters, { number: '', inboundNo: '', warehouse: '', destination: '', status: '', start: '', end: '' }) }
|
||||
function resetForm() {
|
||||
@@ -209,17 +263,44 @@ 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 }
|
||||
}
|
||||
|
||||
function returnToList() {
|
||||
if (props.createPage || props.detailPage) {
|
||||
const routeName = ({ labeling: 'overseas-labeling', transfer: 'overseas-storage-transfer', disposal: 'overseas-cargo-disposal', relabel: 'overseas-return-relabel' } as Record<ServiceType, string>)[props.service]
|
||||
void router.push({ name: routeName })
|
||||
return
|
||||
}
|
||||
mode.value = 'list'
|
||||
}
|
||||
|
||||
async function openEdit(transferNo: string) {
|
||||
try {
|
||||
resetForm()
|
||||
@@ -280,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) => `<section class="label"><div class="title">仓储转运客户箱唛</div><div class="mark">${escapePrintHtml(label.mark)}</div><div class="sku">SKU:${escapePrintHtml(label.sku)}</div><div class="meta">${label.index} / ${labels.length}</div></section>`).join('')
|
||||
printWindow.document.open()
|
||||
printWindow.document.write(`<!doctype html><html><head><meta charset="utf-8"><title>仓储转运客户箱唛</title><style>@page{size:100mm 100mm;margin:8mm}body{margin:0;font-family:Arial,'Microsoft YaHei',sans-serif;color:#111}.label{height:84mm;display:flex;flex-direction:column;align-items:center;justify-content:center;page-break-after:always;border:1px solid #222;box-sizing:border-box;padding:8mm}.title{font-size:18px;font-weight:700}.mark{margin:11mm 0 6mm;font-size:26px;font-weight:700;letter-spacing:1px;word-break:break-all;text-align:center}.sku{font-size:17px;font-weight:650}.meta{font-size:12px;margin-top:4mm}</style></head><body>${sections}</body></html>`)
|
||||
printWindow.document.close(); printWindow.focus(); printWindow.print()
|
||||
}
|
||||
function addTransferRelabelRow() { transferRelabelRows.value.push(createCargoRow()) }
|
||||
function clearTransferRelabelRows() {
|
||||
transferRelabelRows.value = []
|
||||
@@ -308,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
|
||||
@@ -606,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<string, string> = {
|
||||
@@ -670,10 +788,51 @@ async function load() {
|
||||
} catch (error) { submittedRows.value = []; total.value = 0; ElMessage.error(error instanceof Error ? error.message : '列表加载失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function query() { page.value = 1; void load() }
|
||||
function changePage(value: number) { page.value = value; void load() }
|
||||
function changePageSize(value: number) { pageSize.value = value; page.value = 1; void load() }
|
||||
function query() { if (page.value === 1) void load(); else page.value = 1 }
|
||||
function resetListQuery() { resetFilters(); query() }
|
||||
function listRowActions(candidate: object): TableAction[] {
|
||||
const item = candidate as Record<string, string>
|
||||
const actions: TableAction[] = [{ key: 'detail', label: '详情', run: () => openDetail(item) }]
|
||||
if (canCreate.value && props.service === 'transfer' && item.__status === 'DRAFT') actions.push({ key: 'edit', label: '编辑', run: () => openEdit(item['0']) })
|
||||
if (canConfirmTransfer.value && props.service === 'transfer' && item.__status === 'WAIT_CUSTOMER_CONFIRMATION') actions.push({ key: 'confirm-measurement', label: '确认测量', run: () => confirmTransferFromList(item['0']) })
|
||||
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'
|
||||
@@ -690,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
|
||||
}
|
||||
@@ -725,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 }
|
||||
@@ -752,12 +919,14 @@ async function submit(action: 'draft' | 'submit' = 'submit') {
|
||||
if (shippingLabelFile.value) await uploadRelabelShippingLabel(created.application.relabelNo, shippingLabelFile.value)
|
||||
} catch (error) {
|
||||
// Creation already committed: leave the creation form even if the detail reload fails.
|
||||
mode.value = 'list'
|
||||
await load()
|
||||
await openDetail({ '0': created.application.relabelNo })
|
||||
if (props.createPage) await router.push({ name: 'overseas-labeling-detail', params: { detailNo: created.application.relabelNo } })
|
||||
else { mode.value = 'list'; await load(); await openDetail({ '0': created.application.relabelNo }) }
|
||||
await ElMessageBox.alert(`申请 ${created.application.relabelNo} 已创建,部分附件上传失败:${error instanceof Error ? error.message : '上传失败'}。请在原单详情补传失败附件,不要重复下单。`, '申请已创建,附件待补传', { confirmButtonText: '知道了' })
|
||||
return
|
||||
}
|
||||
ElMessage.success(`${config.value.title}提交成功`)
|
||||
await router.push({ name: 'overseas-labeling-detail', params: { detailNo: created.application.relabelNo } })
|
||||
return
|
||||
} else if (props.service === 'disposal') {
|
||||
const items = cargoRows.value.filter((item) => item.quantity > 0).map((item) => {
|
||||
if (!item.sourceOrderId || item.sourceOrderId !== selectedDiscardOrderId.value || item.quantity > Number(item.availableQuantity || 0)) {
|
||||
@@ -779,17 +948,37 @@ 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
|
||||
}
|
||||
}
|
||||
ElMessage.success(props.service === 'transfer' ? '转运单草稿保存成功' : `${config.value.title}提交成功`); mode.value = 'list'; await load()
|
||||
ElMessage.success(props.service === 'transfer' ? '转运单草稿保存成功' : `${config.value.title}提交成功`)
|
||||
if (props.createPage) await router.push({ name: props.service === 'relabel' ? 'overseas-return-relabel' : 'overseas-labeling' })
|
||||
else { mode.value = 'list'; await load() }
|
||||
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '提交失败') }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
async function openDetail(value: Record<string, string>) {
|
||||
if (['labeling', 'transfer', 'disposal'].includes(props.service) && !props.detailPage) {
|
||||
const routeName = ({ labeling: 'overseas-labeling-detail', transfer: 'overseas-storage-transfer-detail', disposal: 'overseas-cargo-disposal-detail' } as Partial<Record<ServiceType, string>>)[props.service]
|
||||
if (routeName) void router.push({ name: routeName, params: { detailNo: value['0'] } })
|
||||
return
|
||||
}
|
||||
detailError.value = ''
|
||||
try {
|
||||
const detail = props.service === 'labeling' ? await getRelabel(value['0']) : props.service === 'disposal' ? await getDiscard(value['0']) : await getTransfer(value['0'])
|
||||
const discardSummary = props.service === 'disposal' ? (detail as { summary?: ServiceDetail }).summary : undefined
|
||||
@@ -798,7 +987,11 @@ async function openDetail(value: Record<string, string>) {
|
||||
mode.value = 'detail'
|
||||
if (props.service === 'labeling') void loadCompletionPhotos()
|
||||
if (props.service === 'transfer') void loadMeasurementPhotos()
|
||||
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '详情加载失败') }
|
||||
} 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() {
|
||||
await loadWarehouses()
|
||||
await openDetail({ '0': detailNumber.value })
|
||||
}
|
||||
async function confirmTransferMeasurements() {
|
||||
const transferNo = String(selectedDetail.value?.transferNo || selectedDetail.value?.['0'] || '')
|
||||
@@ -963,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<string, ReturnRelabelSkuRow>()
|
||||
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),
|
||||
@@ -989,53 +1198,36 @@ 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()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="warehouse-service-page">
|
||||
<section class="warehouse-service-page" :class="{ 'warehouse-service-page--detail': mode === 'detail', 'warehouse-service-page--return-relabel': isReturnRelabelSource }">
|
||||
<template v-if="mode === 'list'">
|
||||
<div class="warehouse-breadcrumb"><span>首页</span><b>/</b><strong>{{ config.title }}</strong></div>
|
||||
<div class="warehouse-page-heading"><h2>{{ config.title }}</h2><el-button v-if="canCreate" type="primary" @click="openCreate"><Plus :size="16" />{{ props.service === 'relabel' ? '新建退货换标单' : props.service === 'transfer' ? '新增订单' : `新增${config.title}` }}</el-button></div>
|
||||
<div class="warehouse-card warehouse-filter-card">
|
||||
<div class="warehouse-filter-grid">
|
||||
<label><span>{{ config.numberLabel }}</span><el-input v-model="filters.number" :placeholder="`请输入${config.numberLabel}`" clearable /></label>
|
||||
<label v-if="props.service !== 'labeling' && props.service !== 'relabel'"><span>进仓编号</span><el-input v-model="filters.inboundNo" placeholder="请输入进仓编号" clearable /></label>
|
||||
<label><span>{{ props.service === 'relabel' ? '来源 FBA 仓' : '操作仓' }}</span><el-select v-model="filters.warehouse" clearable :loading="fbaWarehousesLoading" placeholder="全部"><el-option label="全部" value="" /><el-option v-for="warehouse in (props.service === 'relabel' ? fbaWarehouses : operationWarehouses)" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="String(warehouse.id)" /></el-select></label>
|
||||
<label v-if="props.service === 'transfer' || props.service === 'relabel'"><span>目的 FBA 仓</span><el-input v-model="filters.destination" placeholder="请输入仓代码" clearable /></label>
|
||||
<label><span>订单状态</span><el-select v-model="filters.status" placeholder="全部状态" clearable><el-option v-for="option in statusOptions" :key="option.value" :label="option.label" :value="option.value" /></el-select></label>
|
||||
<label><span>创建时间</span><el-date-picker v-model="filters.start" type="date" placeholder="开始日期" value-format="YYYY-MM-DD" /></label>
|
||||
<label><span> </span><el-date-picker v-model="filters.end" type="date" placeholder="结束日期" value-format="YYYY-MM-DD" /></label>
|
||||
</div>
|
||||
<div class="warehouse-filter-actions"><el-button type="primary" @click="query"><Search :size="16" />查询</el-button><el-button @click="resetFilters(); query()">重置</el-button></div>
|
||||
</div>
|
||||
<div v-loading="loading" class="warehouse-card warehouse-table-card">
|
||||
<div v-if="props.service === 'transfer' && submittedRows.some((item) => item.__status === 'WAIT_CUSTOMER_CONFIRMATION')" class="warehouse-hint">部分入库完成订单已有 PDA 实测数据,请在对应行确认测量数据。</div>
|
||||
<div class="warehouse-table-scroll"><table><thead><tr><th v-for="column in config.columns" :key="column">{{ column }}</th></tr></thead><tbody><tr v-for="(row, rowIndex) in submittedRows" :key="rowIndex"><td><div class="warehouse-list-actions"><button class="warehouse-link" type="button" @click="openDetail(row)"><Eye :size="15" />查看</button><el-button v-if="canCreate && props.service === 'transfer' && row.__status === 'DRAFT'" native-type="button" size="small" type="primary" @click="openEdit(row['0'])">编辑</el-button><el-button v-if="canConfirmTransfer && props.service === 'transfer' && row.__status === 'WAIT_CUSTOMER_CONFIRMATION'" size="small" type="primary" @click="confirmTransferFromList(row['0'])">确认测量数据</el-button><el-button v-if="canConfirmRelabel && props.service === 'labeling' && row.__status === 'CUSTOMER_CONFIRM_PENDING' && !row.__confirmedAt" size="small" type="primary" @click="confirmRelabelFromList(row['0'])">换标完成确认</el-button></div></td><td v-for="(_, index) in config.columns.slice(1)" :key="index"><SingleLineText :value="row[String(index)]" /></td></tr><tr v-if="submittedRows.length === 0"><td :colspan="config.columns.length"><div class="warehouse-empty">{{ config.emptyText }}</div></td></tr></tbody></table></div>
|
||||
<div class="warehouse-pagination">
|
||||
<span class="warehouse-pagination-total">共 {{ total }} 条</span>
|
||||
<el-select v-model="pageSize" class="warehouse-page-size-select" aria-label="每页条数" @change="changePageSize"><el-option v-for="size in [15, 30, 50, 100]" :key="size" :label="`${size}条/页`" :value="size" /></el-select>
|
||||
<el-pagination v-model:current-page="page" :page-size="pageSize" :total="total" :hide-on-single-page="false" layout="prev, pager, next" @current-change="changePage" />
|
||||
</div>
|
||||
<QueryToolbar v-model="listQueryModel" layout="grid" :ariaLabel="`${config.title}筛选`" :fields="listQueryFields" :search-on-reset="false" @search="query" @reset="resetListQuery" />
|
||||
<div class="warehouse-card warehouse-table-card">
|
||||
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="submittedRows" :columns="listColumns" :loading="loading" :total="total" :page-sizes="[15, 30, 50, 100]" :row-actions="listRowActions" :empty-text="config.emptyText">
|
||||
<template #toolbar-actions><el-button v-if="canCreate" type="primary" @click="openCreate"><Plus :size="16" />{{ props.service === 'relabel' ? '新建退货换标单' : props.service === 'transfer' ? '新增订单' : `新增${config.title}` }}</el-button></template>
|
||||
<template v-if="props.service === 'transfer' && submittedRows.some((item) => item.__status === 'WAIT_CUSTOMER_CONFIRMATION')" #before-table><div class="warehouse-list-notice">部分入库完成订单已有 PDA 实测数据,请在对应行确认测量数据。</div></template>
|
||||
</DataTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="mode === 'create' || mode === 'edit'">
|
||||
<div class="warehouse-breadcrumb"><button type="button" @click="mode = 'list'">{{ config.title }}</button><b>/</b><strong>{{ mode === 'edit' ? '编辑' : '新增' }}</strong></div>
|
||||
<div class="warehouse-page-heading"><h2>{{ mode === 'edit' ? `编辑转运草稿 ${editingTransferNo}` : config.createTitle }}</h2><el-button @click="mode = 'list'"><ArrowLeft :size="16" />返回列表</el-button></div>
|
||||
<div class="warehouse-page-heading"><h2>{{ mode === 'edit' ? `编辑转运草稿 ${editingTransferNo}` : config.createTitle }}</h2><el-button @click="returnToList"><ArrowLeft :size="16" />返回列表</el-button></div>
|
||||
<form :class="{ 'warehouse-transfer-form': props.service === 'transfer', 'warehouse-transfer-form--relabel': props.service === 'transfer' && form.relabel }" @submit.prevent="submit('submit')">
|
||||
<div class="warehouse-card warehouse-form-section warehouse-transfer-main-card"><h3>{{ props.service === 'transfer' ? '入库与转运信息' : props.service === 'relabel' ? '退货信息' : props.service === 'labeling' ? '订单信息' : '基础信息' }}</h3>
|
||||
<div class="warehouse-card warehouse-form-section warehouse-transfer-main-card"><h3>{{ props.service === 'transfer' ? '转运信息' : props.service === 'relabel' ? '退货信息' : props.service === 'labeling' ? '订单信息' : '基础信息' }}</h3>
|
||||
<div class="warehouse-form-grid">
|
||||
<label v-if="props.service === 'disposal'" class="warehouse-span-2"><span><i>*</i>操作仓</span><el-select v-model="form.warehouse" filterable clearable :loading="fbaWarehousesLoading" :disabled="Boolean(operationWarehousesError)" placeholder="请选择海外仓" @change="selectDiscardWarehouse"><el-option v-for="warehouse in operationWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="String(warehouse.id)" /></el-select><small v-if="operationWarehousesError" class="warehouse-hint">{{ operationWarehousesError }}</small><small v-else class="warehouse-hint">请先选择本次废弃货物所在的海外仓。</small></label>
|
||||
<label v-if="props.service === 'disposal'" class="warehouse-span-2"><span><i>*</i>选择入库单</span><el-select v-model="selectedDiscardOrderId" filterable clearable :loading="discardCandidatesLoading" :disabled="!form.warehouse" no-data-text="当前海外仓暂无可废弃库存" placeholder="请先选择操作仓,再选择有可用库存的入库单" @change="selectDiscardInbound"><el-option v-for="candidate in discardCandidates" :key="candidate.orderId" :label="`${candidate.inboundOrderNo || candidate.inboundCode || '—'}|${candidate.inboundCode || '—'}|可用 ${candidate.availablePackages} 箱`" :value="candidate.orderId" /></el-select><small class="warehouse-hint">仅显示当前操作仓下一件代发中仍有可用箱数的入库单。</small></label>
|
||||
<label v-if="props.service === 'relabel'"><span><i>*</i>进仓编号</span><el-input v-model="form.inboundNo" placeholder="请输入已有入仓编号" /></label>
|
||||
<label v-if="props.service === 'relabel'"><span><i>*</i>来源 FBA 仓库</span><el-select v-model="form.sourceWarehouse" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" placeholder="请选择平台维护的 FBA 仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select></label>
|
||||
<label v-else-if="props.service === 'disposal'"><span>进仓编号</span><el-input v-model="form.inboundNo" readonly /></label>
|
||||
<label v-else><span><i>*</i>{{ props.service === 'labeling' ? '海外操作仓(中转仓)' : props.service === 'transfer' ? '入库海外仓' : '操作仓' }}</span><el-select v-model="form.warehouse" :loading="fbaWarehousesLoading || operationWarehouseDetailLoading" :disabled="Boolean(operationWarehousesError)" clearable :placeholder="props.service === 'transfer' ? '请选择海外仓' : '请选择国外中转仓'" @change="props.service === 'labeling' ? selectOperationWarehouse() : props.service === 'transfer' && selectTransferWarehouse()"><el-option v-for="warehouse in operationWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="String(warehouse.id)" /></el-select><small v-if="operationWarehousesError" class="warehouse-hint">{{ operationWarehousesError }}</small><small v-else-if="props.service === 'labeling'" class="warehouse-hint">选择后仅查询当前客户在该中转仓的可用库存。</small><small v-else-if="props.service === 'transfer'" class="warehouse-hint">本批货物将先进入该海外仓完成签收和入库。</small></label>
|
||||
<label v-if="props.service === 'labeling'" class="warehouse-span-2"><span><i>*</i>选择有库存的历史入仓单</span><el-select v-model="selectedRelabelInboundOrderId" filterable clearable :loading="relabelCandidatesLoading" :disabled="!form.warehouse" no-data-text="当前中转仓暂无已入库可用库存" placeholder="请选择曾经下单且当前仍有库存的入仓单" @change="selectRelabelInventory"><el-option v-for="option in relabelInboundOptions" :key="option.value" :label="`${option.inboundOrderNo}|入仓码 ${option.inboundCode}|可用 ${option.availablePackages} 箱`" :value="option.value" /></el-select><small class="warehouse-hint">仅显示当前客户在所选中转仓已经完成入库且仍有可用库存的历史入仓单。</small></label>
|
||||
<label v-if="props.service === 'labeling' && labelingMode !== 'sku'"><span><i>*</i>FBA 目的仓</span><el-select v-model="form.destination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder="请选择本张申请统一的 FBA 目的仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select><small class="warehouse-hint">整张贴标申请只能选择一个目的仓,全部明细统一使用该目的仓。</small></label>
|
||||
<div v-if="props.service === 'transfer'" class="warehouse-choice-field warehouse-span-2"><span><i>*</i>货物如何送到海外仓</span><el-radio-group v-model="form.deliveryMethod" @change="selectTransferInboundMethod"><el-radio-button value="WAREHOUSE_PICKUP">仓库上门提货</el-radio-button><el-radio-button value="CUSTOMER_DELIVERY">客户送货到仓</el-radio-button></el-radio-group><small>仓库上门提货:填写货物当前地址;客户送货到仓:下方自动展示海外仓收货信息。</small></div>
|
||||
<label v-else><span><i>*</i>{{ props.service === 'labeling' ? '海外操作仓(中转仓)' : props.service === 'transfer' ? '入库海外仓' : '操作仓' }}</span><el-select v-model="form.warehouse" :loading="fbaWarehousesLoading || operationWarehouseDetailLoading" :disabled="Boolean(operationWarehousesError)" clearable :placeholder="props.service === 'transfer' ? '请选择海外仓' : '请选择国外中转仓'" @change="props.service === 'labeling' ? selectOperationWarehouse() : props.service === 'transfer' && selectTransferWarehouse()"><el-option v-for="warehouse in operationWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="String(warehouse.id)" /></el-select><small v-if="operationWarehousesError" class="warehouse-hint">{{ operationWarehousesError }}</small></label>
|
||||
<label v-if="props.service === 'labeling'" class="warehouse-span-2"><span><i>*</i>选择有库存的入仓单或退货单</span><el-select v-model="selectedRelabelInboundOrderId" filterable clearable :loading="relabelCandidatesLoading" :disabled="!form.warehouse" no-data-text="当前中转仓暂无可用库存" placeholder="请选择当前仍有可用库存的入仓单或退货单" @change="selectRelabelInventory"><el-option v-for="option in relabelInboundOptions" :key="option.value" :label="`${option.inboundOrderNo}|${option.inboundCode}|可用 ${option.availablePackages} ${option.quantityUnit}`" :value="option.value" /></el-select></label>
|
||||
<label v-if="props.service === 'labeling' && (isReturnRelabelSource || labelingMode !== 'sku')"><span><i>*</i>FBA 目的仓</span><el-select v-model="form.destination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder="请选择本张申请统一的 FBA 目的仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select></label>
|
||||
<div v-if="props.service === 'transfer'" class="warehouse-choice-field warehouse-span-2"><span><i>*</i>入仓方式</span><el-radio-group v-model="form.deliveryMethod" @change="selectTransferInboundMethod"><el-radio-button value="WAREHOUSE_PICKUP">仓库上门提货</el-radio-button><el-radio-button value="CUSTOMER_DELIVERY">客户送货到仓</el-radio-button></el-radio-group></div>
|
||||
<label v-if="props.service === 'transfer' && form.deliveryMethod === 'WAREHOUSE_PICKUP'" class="pickup-address-field"><span><i>*</i>提货地址</span><el-input v-model="form.address" placeholder="请输入仓库上门提货的详细地址" /></label>
|
||||
<label v-if="props.service === 'transfer' && form.deliveryMethod === 'WAREHOUSE_PICKUP'"><span><i>*</i>提货联系人</span><el-input v-model="form.pickupContact" placeholder="请输入提货联系人" maxlength="128" /></label>
|
||||
<label v-if="props.service === 'transfer' && form.deliveryMethod === 'WAREHOUSE_PICKUP'"><span><i>*</i>提货联系人电话</span><el-input v-model="form.pickupPhone" placeholder="请输入提货联系人电话" maxlength="64" /></label>
|
||||
@@ -1051,15 +1243,15 @@ function selectRelabelInventory(inboundOrderId: string) {
|
||||
<label v-if="props.service === 'labeling'"><span><i>*</i>海外仓签收联系人<em v-if="form.warehouse && !form.contact" class="warehouse-unmaintained">未维护</em></span><el-input v-model="form.contact" placeholder="选择海外操作仓后自动带出" /></label>
|
||||
<label v-if="props.service === 'labeling'"><span><i>*</i>海外仓联系电话<em v-if="form.warehouse && !form.phone" class="warehouse-unmaintained">未维护</em></span><el-input v-model="form.phone" placeholder="选择海外操作仓后自动带出" /></label>
|
||||
<label v-if="props.service === 'labeling'"><span><i>*</i>贴标截止时间</span><el-date-picker v-model="form.labelDeadline" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择截止时间" /></label>
|
||||
<label v-if="props.service === 'labeling' && labelingMode !== 'sku'"><span><i>*</i>换标后派送方式</span><el-select v-model="form.deliveryMethod" placeholder="请选择派送方式"><el-option label="卡车派送" value="TRUCK" /><el-option label="快递派送" value="EXPRESS" /><el-option label="卡车自提" value="TRUCK_PICKUP" /><el-option label="快递自提" value="EXPRESS_PICKUP" /></el-select><small class="warehouse-hint">卡车派送进入预排车;其余方式确认换标完成后进入换标出库。</small></label>
|
||||
<label v-if="props.service === 'labeling' && labelingMode !== 'sku'"><span><i>*</i>换标后派送方式</span><el-select v-model="form.deliveryMethod" placeholder="请选择派送方式"><el-option label="卡车派送" value="TRUCK" /><el-option label="快递派送" value="EXPRESS" /><el-option label="卡车自提" value="TRUCK_PICKUP" /><el-option label="快递自提" value="EXPRESS_PICKUP" /></el-select></label>
|
||||
<div v-if="props.service === 'labeling' && labelingMode !== 'sku' && (form.deliveryMethod === 'EXPRESS' || form.deliveryMethod === 'EXPRESS_PICKUP')" class="warehouse-checkbox-field"><span>是否打印面单</span><el-checkbox v-model="form.relabel">需要仓库打印面单</el-checkbox></div>
|
||||
<label v-if="canManageRelabelFiles && props.service === 'labeling' && labelingMode !== 'sku' && (form.deliveryMethod === 'EXPRESS' || form.deliveryMethod === 'EXPRESS_PICKUP') && form.relabel"><span><i>*</i>上传面单文件</span><input class="warehouse-native-file" type="file" accept="application/pdf,image/*,.pdf" @change="selectShippingLabel"><small class="warehouse-hint">支持 PDF 或图片,最大 20 MB。</small></label>
|
||||
<label v-if="canManageRelabelFiles && props.service === 'labeling' && labelingMode !== 'sku' && (form.deliveryMethod === 'EXPRESS' || form.deliveryMethod === 'EXPRESS_PICKUP') && form.relabel"><span><i>*</i>上传面单文件</span><input class="warehouse-native-file" type="file" accept="application/pdf,image/*,.pdf" @change="selectShippingLabel"></label>
|
||||
<label v-if="props.service === 'relabel'" class="warehouse-span-2"><span><i>*</i>详细派送地址</span><el-input v-model="form.address" placeholder="请输入派送地址" /></label>
|
||||
<label v-if="props.service === 'transfer' || props.service === 'relabel'"><span><i>*</i>预估总体积(CBM)</span><el-input-number v-model="form.volumeCbm" :min="0" :precision="4" :step="0.0001" controls-position="right" placeholder="请输入预估总体积" /></label>
|
||||
<label v-if="props.service === 'transfer' || props.service === 'relabel'"><span><i>*</i>{{ props.service === 'transfer' ? '预估体积(CBM)' : '预估总体积(CBM)' }}</span><el-input-number v-model="form.volumeCbm" :min="0" :precision="4" :step="0.0001" controls-position="right" placeholder="请输入预估总体积" /></label>
|
||||
<label v-if="props.service === 'transfer' || props.service === 'relabel'"><span><i>*</i>{{ props.service === 'transfer' ? '预计入仓时间' : '预计到达时间' }}</span><el-date-picker v-model="form.estimatedArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" :placeholder="props.service === 'transfer' ? '请选择预计入仓时间' : '请选择预计到达时间'" /></label>
|
||||
<label v-if="props.service === 'transfer'"><span>预计发货时间</span><el-date-picker v-model="form.estimatedShipAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="选填,可线下确认后补充" /><small class="warehouse-hint">暂未确定时可不填写,不影响货物先入海外仓。</small></label>
|
||||
<div v-if="props.service === 'transfer'" class="warehouse-choice-field warehouse-span-2"><span><i>*</i>是否有目的去向</span><el-radio-group v-model="transferDestinationType" @change="selectTransferDestinationType"><el-radio-button value="NONE">暂未确定</el-radio-button><el-radio-button value="FBA">FBA 仓</el-radio-button><el-radio-button value="PRIVATE">私人仓</el-radio-button></el-radio-group><small>暂未确定也可以提交;完成入库后可在新建出库单中批量绑定目的地。</small></div>
|
||||
<label v-if="props.service === 'transfer' && transferDestinationType === 'FBA'"><span><i>*</i>未换标货物目的 FBA 仓</span><el-select v-model="form.destination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder="请选择平台维护的 FBA 仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select><small class="warehouse-hint">此处只表示不换标箱子的去向;需要换箱唛的箱子在换标明细中单独选择目的仓。</small></label>
|
||||
<label v-if="props.service === 'transfer'"><span>预计发货时间</span><el-date-picker v-model="form.estimatedShipAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="选填" /></label>
|
||||
<div v-if="props.service === 'transfer'" class="warehouse-choice-field warehouse-span-2"><span><i>*</i>目的去向</span><el-radio-group v-model="transferDestinationType" @change="selectTransferDestinationType"><el-radio-button value="NONE">暂未确定</el-radio-button><el-radio-button value="FBA">FBA 仓</el-radio-button><el-radio-button value="PRIVATE">私人仓</el-radio-button></el-radio-group></div>
|
||||
<label v-if="props.service === 'transfer' && transferDestinationType === 'FBA'"><span><i>*</i>FBA 目的仓</span><el-select v-model="form.destination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder="请选择 FBA 仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select></label>
|
||||
<label v-if="props.service === 'transfer' && transferDestinationType === 'FBA'"><span>FBA 货件编号</span><el-input v-model="form.fbaShipmentNo" placeholder="选填,可后续补充" /></label>
|
||||
<label v-if="props.service === 'transfer' && transferDestinationType === 'PRIVATE'" class="warehouse-span-2"><span><i>*</i>私人仓地址</span><el-input v-model="form.privateDestinationAddress" maxlength="1000" placeholder="请输入私人仓完整地址" /></label>
|
||||
<label v-if="props.service === 'transfer' && transferDestinationType === 'PRIVATE'"><span><i>*</i>邮编</span><el-input v-model="form.privateDestinationPostalCode" maxlength="32" placeholder="请输入邮编" /></label>
|
||||
@@ -1068,66 +1260,86 @@ function selectRelabelInventory(inboundOrderId: string) {
|
||||
<label v-if="props.service === 'relabel'"><span><i>*</i>FBA 货件编号</span><el-input v-model="form.fbaShipmentNo" placeholder="请输入 FBA 货件编号" /></label>
|
||||
<label v-if="props.service === 'labeling'"><span>贴标要求</span><el-input v-model="form.requirement" placeholder="请输入贴标要求" /></label>
|
||||
<label v-else-if="props.service === 'relabel' || (props.service === 'transfer' && form.deliveryMethod === 'CUSTOMER_DELIVERY')"><span>物流跟踪号</span><el-input v-model="form.trackingNo" placeholder="请输入物流跟踪号(选填)" /></label>
|
||||
<label class="warehouse-span-2"><span>订单备注</span><el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注信息" /></label>
|
||||
<div v-if="props.service === 'transfer'" class="warehouse-choice-field warehouse-span-2"><span><i>*</i>是否需要换标</span><el-radio-group v-model="form.relabel"><el-radio-button :value="false">不需要换标</el-radio-button><el-radio-button :value="true">需要换标</el-radio-button></el-radio-group><small>{{ form.relabel ? '本次提交将同时登记换标申请,入库完成后自动进入换标作业。' : '入库完成且已有目的仓后,可直接进入排车流程。' }}</small></div>
|
||||
<label class="warehouse-span-2"><span>备注</span><el-input v-model="form.remark" type="textarea" :rows="2" placeholder="选填" /></label>
|
||||
<div v-if="props.service === 'transfer'" class="warehouse-choice-field warehouse-span-2"><span><i>*</i>换标服务</span><el-radio-group v-model="form.relabel"><el-radio-button :value="false">不需要</el-radio-button><el-radio-button :value="true">需要</el-radio-button></el-radio-group></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="warehouse-card warehouse-form-section warehouse-transfer-cargo-card">
|
||||
<template v-if="props.service === 'labeling'">
|
||||
<div class="warehouse-section-heading warehouse-labeling-heading">
|
||||
<h3>货物明细</h3>
|
||||
<div class="warehouse-template-actions">
|
||||
<el-button v-if="canManageRelabelFiles" size="small" @click="chooseCreateTotalFile('CARTON_TOTAL')"><Upload :size="15" />上传箱唛总文件</el-button>
|
||||
<el-button v-if="canManageRelabelFiles" size="small" @click="chooseCreateTotalFile('SKU_TOTAL')"><Upload :size="15" />上传 SKU 总文件</el-button>
|
||||
<el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('sku')"><Download :size="15" />下载 SKU 关联模板</el-button>
|
||||
<el-button size="small" :disabled="!cargoRows.length" @click="skuTemplateInput?.click()"><Upload :size="15" />上传 SKU 关联</el-button>
|
||||
<el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('carton')"><Download :size="15" />下载箱号关联模板</el-button>
|
||||
<el-button size="small" :disabled="!cargoRows.length" @click="cartonTemplateInput?.click()"><Upload :size="15" />上传箱号关联</el-button>
|
||||
<el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('mixed')"><Download :size="15" />下载 SKU 混箱模板</el-button>
|
||||
<el-button size="small" :disabled="!cargoRows.length" @click="mixedTemplateInput?.click()"><Upload :size="15" />上传 SKU 混箱</el-button>
|
||||
<input ref="skuTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'sku')">
|
||||
<input ref="cartonTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'carton')">
|
||||
<input ref="mixedTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'mixed')">
|
||||
<input ref="cartonTotalCreateInput" class="warehouse-hidden-input" type="file" accept="application/pdf,.pdf" @change="selectCartonTotalFile">
|
||||
<input ref="skuTotalCreateInput" class="warehouse-hidden-input" type="file" accept="application/pdf,.pdf" @change="selectSkuTotalFile">
|
||||
</div>
|
||||
<div><h3>货物明细</h3></div>
|
||||
</div>
|
||||
<div v-if="canManageRelabelFiles" class="warehouse-upload-summary">
|
||||
<span>箱唛总文件:{{ cartonTotalFile?.name || '未选择' }}<button v-if="cartonTotalFile" type="button" class="warehouse-file-clear" title="清除箱唛总文件" aria-label="清除箱唛总文件" :disabled="saving" @click="clearCreateTotalFile('CARTON_TOTAL')"><X :size="14" /></button></span>
|
||||
<span>SKU 总文件:{{ skuTotalFile?.name || '未选择' }}<button v-if="skuTotalFile" type="button" class="warehouse-file-clear" title="清除 SKU 总文件" aria-label="清除 SKU 总文件" :disabled="saving" @click="clearCreateTotalFile('SKU_TOTAL')"><X :size="14" /></button></span>
|
||||
<div class="warehouse-file-groups" :class="{ 'warehouse-file-groups--single': isReturnRelabelSource }">
|
||||
<section v-if="canManageRelabelFiles" class="warehouse-file-group warehouse-file-group--primary">
|
||||
<div class="warehouse-file-group__heading"><div><strong><span>1</span>贴标总文件</strong></div><em>{{ requiresBothTotalFiles ? '必传 · 两份文件' : '必传 · 二选一' }}</em></div>
|
||||
<div class="warehouse-file-options">
|
||||
<div class="warehouse-file-option" :class="{ 'is-selected': Boolean(cartonTotalFile) }"><div class="warehouse-file-option__name"><strong>箱唛总文件</strong></div><el-button :type="cartonTotalFile ? 'primary' : 'default'" plain @click="chooseCreateTotalFile('CARTON_TOTAL')"><Upload :size="15" />{{ cartonTotalFile ? '重新上传' : '选择文件' }}</el-button><div class="warehouse-file-option__status" :class="{ 'is-empty': !cartonTotalFile }"><span>{{ cartonTotalFile?.name || '尚未选择文件' }}</span><button v-if="cartonTotalFile" type="button" class="warehouse-file-clear" title="清除箱唛总文件" aria-label="清除箱唛总文件" :disabled="saving" @click="clearCreateTotalFile('CARTON_TOTAL')"><X :size="14" /></button></div></div>
|
||||
<div class="warehouse-file-choice-or" :class="{ 'is-both': requiresBothTotalFiles }">{{ requiresBothTotalFiles ? '和' : '或' }}</div>
|
||||
<div class="warehouse-file-option" :class="{ 'is-selected': Boolean(skuTotalFile) }"><div class="warehouse-file-option__name"><strong>SKU 总文件</strong></div><el-button :type="skuTotalFile ? 'primary' : 'default'" plain @click="chooseCreateTotalFile('SKU_TOTAL')"><Upload :size="15" />{{ skuTotalFile ? '重新上传' : '选择文件' }}</el-button><div class="warehouse-file-option__status" :class="{ 'is-empty': !skuTotalFile }"><span>{{ skuTotalFile?.name || '尚未选择文件' }}</span><button v-if="skuTotalFile" type="button" class="warehouse-file-clear" title="清除 SKU 总文件" aria-label="清除 SKU 总文件" :disabled="saving" @click="clearCreateTotalFile('SKU_TOTAL')"><X :size="14" /></button></div></div>
|
||||
</div>
|
||||
</section>
|
||||
<section v-if="!isReturnRelabelSource" class="warehouse-file-group">
|
||||
<div class="warehouse-file-group__heading"><div><strong><span>2</span>明细关联模板</strong></div><em>按需使用</em></div>
|
||||
<div class="warehouse-template-list">
|
||||
<div><span><strong>SKU 关联</strong></span><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('sku')"><Download :size="15" />下载模板</el-button><el-button type="primary" plain size="small" :disabled="!cargoRows.length" @click="skuTemplateInput?.click()"><Upload :size="15" />上传文件</el-button></div>
|
||||
<div><span><strong>箱号关联</strong></span><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('carton')"><Download :size="15" />下载模板</el-button><el-button type="primary" plain size="small" :disabled="!cargoRows.length" @click="cartonTemplateInput?.click()"><Upload :size="15" />上传文件</el-button></div>
|
||||
<div><span><strong>SKU 混箱</strong></span><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('mixed')"><Download :size="15" />下载模板</el-button><el-button type="primary" plain size="small" :disabled="!cargoRows.length" @click="mixedTemplateInput?.click()"><Upload :size="15" />上传文件</el-button></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="warehouse-labeling-mode">
|
||||
<input ref="skuTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'sku')"><input ref="cartonTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'carton')"><input ref="mixedTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'mixed')"><input ref="cartonTotalCreateInput" class="warehouse-hidden-input" type="file" accept="application/pdf,.pdf" @change="selectCartonTotalFile"><input ref="skuTotalCreateInput" class="warehouse-hidden-input" type="file" accept="application/pdf,.pdf" @change="selectSkuTotalFile">
|
||||
<div v-if="isReturnRelabelSource" class="warehouse-table-scroll">
|
||||
<table class="warehouse-edit-table warehouse-return-relabel-table">
|
||||
<thead><tr><th>选择</th><th>SKU</th><th>中文品名</th><th>库存总数</th><th>本次贴标数量</th></tr></thead>
|
||||
<tbody><tr v-for="row in returnRelabelSkuRows" :key="row.skuCode"><td><el-checkbox v-model="row.selected" /></td><td><strong>{{ row.skuCode }}</strong></td><td>{{ row.productNameZh || '—' }}</td><td><strong>{{ row.availableQuantity }}</strong></td><td><el-input-number v-model="row.quantity" :disabled="!row.selected" :min="1" :max="row.availableQuantity" :precision="0" controls-position="right" placeholder="请输入贴标数量" /></td></tr><tr v-if="returnRelabelSkuRows.length === 0"><td colspan="5"><div class="warehouse-empty">该退货单暂无可申请贴标的 SKU 库存</div></td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-if="!isReturnRelabelSource" class="warehouse-labeling-mode">
|
||||
<span>换标类型</span>
|
||||
<el-radio-group v-model="labelingMode" aria-label="换标类型">
|
||||
<el-radio-button value="carton">只换箱唛</el-radio-button>
|
||||
<el-radio-button value="sku">只换 SKU 标签</el-radio-button>
|
||||
<el-radio-button value="carton_and_sku">SKU 和箱唛都换</el-radio-button>
|
||||
</el-radio-group>
|
||||
<p>{{ labelingModeHint }}</p>
|
||||
</div>
|
||||
<div v-if="importedRelabelBindings.length" class="warehouse-import-preview">
|
||||
<div v-if="!isReturnRelabelSource && importedRelabelBindings.length" class="warehouse-import-preview">
|
||||
<div class="warehouse-section-heading"><div><strong>待导入绑定关系</strong><p>可只勾选本次需要换标的部分,也可以全选后一次导入。</p></div><div><el-checkbox :model-value="importedRelabelBindings.every((item) => item.selected)" @change="selectAllImportedBindings(Boolean($event))">全选</el-checkbox><el-button type="primary" size="small" @click="importSelectedBindingsToDetails">导入到明细</el-button></div></div>
|
||||
<div class="warehouse-table-scroll"><table class="warehouse-binding-preview-table"><thead><tr><th>选择</th><th>关系类型</th><th>仓库唛头</th><th>旧 SKU</th><th>新 SKU</th><th>数量</th><th>新 FBA 箱唛</th></tr></thead><tbody><tr v-for="item in importedRelabelBindings" :key="item.id"><td><el-checkbox v-model="item.selected" /></td><td>{{ item.templateType === 'mixed' ? 'SKU 混箱换标' : item.templateType === 'carton' ? '箱号关联' : 'SKU 关联' }}</td><td>{{ item.cartonMark }}</td><td>{{ item.originalSku || '—' }}</td><td>{{ item.newSku || '—' }}</td><td>{{ item.quantity }}</td><td>{{ item.fbaCartonMark || '—' }}</td></tr></tbody></table></div>
|
||||
</div>
|
||||
<div class="warehouse-section-heading warehouse-tab-actions">
|
||||
<div><strong>绑定明细</strong><p>上传后在下方回显,也可以手动添加;SKU 与箱号关联通过仓库唛头合并。系统只提供大概库存位置,不代表箱唛已绑定具体托盘。</p></div>
|
||||
<div v-if="!isReturnRelabelSource" class="warehouse-section-heading warehouse-tab-actions">
|
||||
<div><strong>绑定明细</strong></div>
|
||||
<div v-if="labelingMode === 'carton'"><el-button type="primary" size="small" @click="addRelabelCargoRow"><Plus :size="15" />手动添加行</el-button><el-button size="small" @click="clearRelabelRows">清空</el-button></div>
|
||||
<div v-else><el-button type="primary" size="small" @click="addSkuRelabelRow"><Plus :size="15" />添加 SKU 明细</el-button><el-button size="small" @click="skuRelabelRows = []">清空</el-button></div>
|
||||
</div>
|
||||
<div v-if="labelingMode === 'carton'" class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-label-table"><thead><tr><th>商品 SKU</th><th>中文品名 *</th><th>FBA 货件编号 *</th><th>箱数</th><th>仓库唛头 *</th><th>新 FBA 箱唛 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in cartonDetailRows" :key="row.id"><td><el-input v-model="row.sku" placeholder="选填" /></td><td><el-input v-model="row.name" placeholder="请输入中文品名" /></td><td><el-input v-model="row.shipmentNo" placeholder="请输入FBA货件编号" /></td><td><strong>1 箱</strong></td><td><el-input v-model="row.cartonMark" placeholder="请输入仓库唛头" /></td><td><el-input v-model="row.fbaCartonMark" placeholder="请输入新的 FBA 箱唛" /></td><td><el-input v-model="row.remark" type="textarea" :rows="2" maxlength="1000" show-word-limit placeholder="请输入明细备注" /></td><td><button type="button" class="warehouse-delete" title="移出本次明细" @click="removeCargoRow(row.id)"><Trash2 :size="16" />移出</button></td></tr><tr v-if="cartonDetailRows.length === 0"><td colspan="8"><div class="warehouse-empty">暂无本次换标明细,请上传关系后导入或手动添加</div></td></tr></tbody></table></div>
|
||||
<div v-if="!isReturnRelabelSource && labelingMode === 'carton'" class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-label-table"><thead><tr><th>商品 SKU</th><th>中文品名 *</th><th>FBA 货件编号 *</th><th>箱数</th><th>仓库唛头 *</th><th>新 FBA 箱唛 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in cartonDetailRows" :key="row.id"><td><el-input v-model="row.sku" placeholder="选填" /></td><td><el-input v-model="row.name" placeholder="请输入中文品名" /></td><td><el-input v-model="row.shipmentNo" placeholder="请输入FBA货件编号" /></td><td><strong>1 箱</strong></td><td><el-input v-model="row.cartonMark" placeholder="请输入仓库唛头" /></td><td><el-input v-model="row.fbaCartonMark" placeholder="请输入新的 FBA 箱唛" /></td><td><el-input v-model="row.remark" type="textarea" :rows="2" maxlength="1000" show-word-limit placeholder="请输入明细备注" /></td><td><button type="button" class="warehouse-delete" title="移出本次明细" @click="removeCargoRow(row.id)"><Trash2 :size="16" />移出</button></td></tr><tr v-if="cartonDetailRows.length === 0"><td colspan="8"><div class="warehouse-empty">暂无本次换标明细,请上传关系后导入或手动添加</div></td></tr></tbody></table></div>
|
||||
<div v-else class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-sku-label-table"><thead><tr><th>仓库唛头 *</th><th>旧 SKU *</th><th>新 SKU *</th><th>换标数量(件)*</th><th v-if="labelingMode === 'carton_and_sku'">新 FBA 箱唛 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in skuRelabelRows" :key="row.id"><td><el-input v-model="row.cartonMark" placeholder="请输入仓库唛头" @change="applyExistingCartonBinding(row)" /></td><td><el-input v-model="row.originalSku" placeholder="请输入旧 SKU" @change="applyExistingSkuBinding(row)" /></td><td><el-input v-model="row.newSku" placeholder="请输入新 SKU" /></td><td><el-input-number v-model="row.quantity" :min="1" :precision="0" controls-position="right" placeholder="请输入件数" /></td><td v-if="labelingMode === 'carton_and_sku'"><el-input v-model="row.fbaCartonMark" placeholder="请输入新的 FBA 箱唛" /></td><td><el-input v-model="row.remark" placeholder="请输入备注" /></td><td><button type="button" class="warehouse-delete" title="删除" @click="removeSkuRelabelRow(row.id)"><Trash2 :size="16" />删除</button></td></tr><tr v-if="skuRelabelRows.length === 0"><td :colspan="labelingMode === 'carton_and_sku' ? 7 : 6"><div class="warehouse-empty">暂无绑定明细,请点击“添加 SKU 明细”</div></td></tr></tbody></table></div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="warehouse-section-heading"><h3>货物明细</h3><div v-if="props.service !== 'disposal'"><el-button type="primary" size="small" @click="addCargoRow"><Plus :size="15" />添加行</el-button><el-button size="small" @click="cargoRows = []">清空</el-button></div></div>
|
||||
<div class="warehouse-section-heading" :class="{ 'warehouse-step-heading': props.service === 'transfer' }"><div><h3><span v-if="props.service === 'transfer'" class="warehouse-step-number">1</span>货物明细</h3></div><div v-if="props.service !== 'disposal'" class="warehouse-heading-actions"><el-button v-if="props.service === 'transfer'" size="small" :disabled="!cargoRows.some((row) => row.sku.trim() && row.quantity > 0)" @click="printTransferDraftCartonMarks"><Printer :size="15" />打印唛头</el-button><el-button type="primary" size="small" @click="addCargoRow"><Plus :size="15" />添加行</el-button><el-button size="small" @click="cargoRows = []">清空</el-button></div></div>
|
||||
<div class="warehouse-table-scroll"><table class="warehouse-edit-table"><thead><tr><th>商品 SKU{{ props.service === 'transfer' ? '' : ' *' }}</th><th>中文品名{{ props.service === 'transfer' ? '' : ' *' }}</th><th v-if="props.service === 'disposal'">可用箱数</th><th v-if="props.service === 'relabel'">FNSKU / 箱唛</th><th>{{ props.service === 'disposal' ? '本次废弃箱数' : '预计箱数' }} *</th><th v-if="props.service !== 'disposal'">操作</th></tr></thead><tbody><tr v-for="row in cargoRows" :key="row.id"><td><el-input v-model="row.sku" :readonly="props.service === 'disposal'" :placeholder="props.service === 'transfer' ? '选填' : '请输入商品 SKU'" /></td><td><el-input v-model="row.name" :readonly="props.service === 'disposal'" :placeholder="props.service === 'transfer' ? '选填' : '请输入中文品名'" /></td><td v-if="props.service === 'disposal'"><strong>{{ row.availableQuantity }} 箱</strong></td><td v-if="props.service === 'relabel'"><el-input v-model="row.shippingMark" placeholder="请输入FNSKU或箱唛" /></td><td><el-input-number v-model="row.quantity" :min="props.service === 'disposal' ? 0 : 1" :max="row.availableQuantity" controls-position="right" placeholder="请输入箱数" /></td><td v-if="props.service !== 'disposal'"><button type="button" class="warehouse-delete" title="删除" @click="removeCargoRow(row.id)"><Trash2 :size="16" />删除</button></td></tr><tr v-if="cargoRows.length === 0"><td :colspan="props.service === 'disposal' ? 4 : props.service === 'relabel' ? 5 : 4"><div class="warehouse-empty">{{ props.service === 'disposal' ? '请先选择入库单,系统将自动带出 SKU 和可用箱数' : '暂无货物明细,请点击“添加行”' }}</div></td></tr></tbody></table></div>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="props.service === 'transfer' && form.relabel" class="warehouse-card warehouse-form-section warehouse-transfer-relabel">
|
||||
<div class="warehouse-section-heading warehouse-labeling-heading"><div><h3>货物明细</h3><p>换标明细与贴标申请保持一致;客户只填写自己的箱唛,我方箱唛由系统生成。</p></div><div class="warehouse-template-actions"><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('sku')"><Download :size="15" />下载 SKU 关联模板</el-button><el-button size="small" :disabled="!cargoRows.length" @click="skuTemplateInput?.click()"><Upload :size="15" />上传 SKU 关联</el-button><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('carton')"><Download :size="15" />下载箱号关联模板</el-button><el-button size="small" :disabled="!cargoRows.length" @click="cartonTemplateInput?.click()"><Upload :size="15" />上传箱号关联</el-button><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('mixed')"><Download :size="15" />下载 SKU 混箱模板</el-button><el-button size="small" :disabled="!cargoRows.length" @click="mixedTemplateInput?.click()"><Upload :size="15" />上传 SKU 混箱</el-button><input ref="skuTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'sku')"><input ref="cartonTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'carton')"><input ref="mixedTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'mixed')"></div></div>
|
||||
<div class="warehouse-section-heading warehouse-labeling-heading warehouse-step-heading"><div><h3><span class="warehouse-step-number">2</span>换标申请</h3></div></div>
|
||||
<div class="warehouse-form-grid warehouse-transfer-relabel-fields"><label><span><i>*</i>换标截止时间</span><el-date-picker v-model="form.labelDeadline" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择截止时间" /></label><label><span><i>*</i>换标联系人</span><el-input v-model="form.relabelContact" placeholder="选择海外仓后自动带出,也可手动填写" maxlength="128" /></label><label><span><i>*</i>换标联系人电话</span><el-input v-model="form.relabelPhone" placeholder="选择海外仓后自动带出,也可手动填写" maxlength="64" /></label><label v-if="labelingMode !== 'sku'"><span><i>*</i>换标后派送方式</span><el-select v-model="form.relabelDeliveryMethod" placeholder="请选择派送方式"><el-option label="卡车派送" value="TRUCK" /><el-option label="快递派送" value="EXPRESS" /><el-option label="卡车自提" value="TRUCK_PICKUP" /><el-option label="快递自提" value="EXPRESS_PICKUP" /></el-select></label><label class="warehouse-span-2"><span><i>*</i>整票换标货物目的 FBA 仓</span><el-select v-model="transferRelabelDefaultDestination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder="请选择整票货物的目的仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select><small class="warehouse-hint">本票所有换标货物统一前往该目的仓,暂不支持一票多个目的仓。</small></label><label class="warehouse-span-2"><span>贴标要求</span><el-input v-model="form.requirement" type="textarea" :rows="2" maxlength="1000" show-word-limit placeholder="请输入贴标要求(选填)" /></label></div>
|
||||
<div class="warehouse-labeling-mode"><span>换标类型</span><el-radio-group v-model="labelingMode" aria-label="换标类型"><el-radio-button value="carton">只换箱唛</el-radio-button><el-radio-button value="sku">只换 SKU 标签</el-radio-button><el-radio-button value="carton_and_sku">SKU 和箱唛都换</el-radio-button></el-radio-group><p>{{ labelingModeHint }}</p></div>
|
||||
<div class="warehouse-labeling-mode"><span>换标类型</span><el-radio-group v-model="labelingMode" aria-label="换标类型"><el-radio-button value="carton">只换箱唛</el-radio-button><el-radio-button value="sku">只换 SKU 标签</el-radio-button><el-radio-button value="carton_and_sku">SKU 和箱唛都换</el-radio-button></el-radio-group></div>
|
||||
<div class="warehouse-transfer-file-groups">
|
||||
<section v-if="canManageRelabelFiles" class="warehouse-file-group warehouse-transfer-total-files">
|
||||
<div class="warehouse-file-group__heading"><div><strong>贴标总文件</strong></div><em>{{ requiresBothTotalFiles ? '必传 · 两份文件' : '必传 · 二选一' }}</em></div>
|
||||
<div class="warehouse-file-options">
|
||||
<div class="warehouse-file-option" :class="{ 'is-selected': Boolean(cartonTotalFile) }"><div class="warehouse-file-option__name"><strong>箱唛总文件</strong></div><el-button :type="cartonTotalFile ? 'primary' : 'default'" plain @click="chooseCreateTotalFile('CARTON_TOTAL')"><Upload :size="15" />{{ cartonTotalFile ? '重新上传' : '选择文件' }}</el-button><div class="warehouse-file-option__status" :class="{ 'is-empty': !cartonTotalFile }"><span>{{ cartonTotalFile?.name || '尚未选择文件' }}</span><button v-if="cartonTotalFile" type="button" class="warehouse-file-clear" title="清除箱唛总文件" aria-label="清除箱唛总文件" :disabled="saving" @click="clearCreateTotalFile('CARTON_TOTAL')"><X :size="14" /></button></div></div>
|
||||
<div class="warehouse-file-choice-or" :class="{ 'is-both': requiresBothTotalFiles }">{{ requiresBothTotalFiles ? '和' : '或' }}</div>
|
||||
<div class="warehouse-file-option" :class="{ 'is-selected': Boolean(skuTotalFile) }"><div class="warehouse-file-option__name"><strong>SKU 总文件</strong></div><el-button :type="skuTotalFile ? 'primary' : 'default'" plain @click="chooseCreateTotalFile('SKU_TOTAL')"><Upload :size="15" />{{ skuTotalFile ? '重新上传' : '选择文件' }}</el-button><div class="warehouse-file-option__status" :class="{ 'is-empty': !skuTotalFile }"><span>{{ skuTotalFile?.name || '尚未选择文件' }}</span><button v-if="skuTotalFile" type="button" class="warehouse-file-clear" title="清除 SKU 总文件" aria-label="清除 SKU 总文件" :disabled="saving" @click="clearCreateTotalFile('SKU_TOTAL')"><X :size="14" /></button></div></div>
|
||||
</div>
|
||||
<input ref="cartonTotalCreateInput" class="warehouse-hidden-input" type="file" accept="application/pdf,.pdf" @change="selectCartonTotalFile"><input ref="skuTotalCreateInput" class="warehouse-hidden-input" type="file" accept="application/pdf,.pdf" @change="selectSkuTotalFile">
|
||||
</section>
|
||||
<section class="warehouse-file-group warehouse-transfer-template-group">
|
||||
<div class="warehouse-file-group__heading"><div><strong><span>3</span>明细关联模板</strong></div><em>按需使用</em></div><div class="warehouse-template-list"><div><span><strong>SKU 关联</strong></span><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('sku')"><Download :size="15" />下载模板</el-button><el-button type="primary" plain size="small" :disabled="!cargoRows.length" @click="skuTemplateInput?.click()"><Upload :size="15" />上传文件</el-button></div><div><span><strong>箱号关联</strong></span><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('carton')"><Download :size="15" />下载模板</el-button><el-button type="primary" plain size="small" :disabled="!cargoRows.length" @click="cartonTemplateInput?.click()"><Upload :size="15" />上传文件</el-button></div><div><span><strong>SKU 混箱</strong></span><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('mixed')"><Download :size="15" />下载模板</el-button><el-button type="primary" plain size="small" :disabled="!cargoRows.length" @click="mixedTemplateInput?.click()"><Upload :size="15" />上传文件</el-button></div></div>
|
||||
<input ref="skuTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'sku')"><input ref="cartonTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'carton')"><input ref="mixedTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'mixed')">
|
||||
</section>
|
||||
</div>
|
||||
<div v-if="importedRelabelBindings.length" class="warehouse-import-preview"><div class="warehouse-section-heading"><div><strong>待导入绑定关系</strong><p>可只勾选本次需要换标的部分,也可以全选后一次导入。</p></div><div><el-checkbox :model-value="importedRelabelBindings.every((item) => item.selected)" @change="selectAllImportedBindings(Boolean($event))">全选</el-checkbox><el-button type="primary" size="small" @click="importSelectedBindingsToDetails">导入到明细</el-button></div></div><div class="warehouse-table-scroll"><table class="warehouse-binding-preview-table"><thead><tr><th>选择</th><th>关系类型</th><th>客户箱唛号</th><th>旧 SKU</th><th>新 SKU</th><th>数量</th><th>新 FBA 箱唛</th></tr></thead><tbody><tr v-for="item in importedRelabelBindings" :key="item.id"><td><el-checkbox v-model="item.selected" /></td><td>{{ item.templateType === 'mixed' ? 'SKU 混箱换标' : item.templateType === 'carton' ? '箱号关联' : 'SKU 关联' }}</td><td>{{ item.cartonMark }}</td><td>{{ item.originalSku || '—' }}</td><td>{{ item.newSku || '—' }}</td><td>{{ item.quantity }}</td><td>{{ item.fbaCartonMark || '—' }}</td></tr></tbody></table></div></div>
|
||||
<div class="warehouse-section-heading warehouse-tab-actions"><div><strong>绑定明细</strong><p>上传后在下方回显,也可以手动添加;客户箱唛作为仓库唛头来源,系统我方箱唛不需要手填。</p></div><div><el-button type="primary" size="small" @click="addTransferRelabelRow"><Plus :size="15" />添加 SKU 明细</el-button><el-button size="small" @click="clearTransferRelabelRows">清空</el-button></div></div>
|
||||
<div class="warehouse-section-heading warehouse-tab-actions warehouse-binding-heading"><div><strong><span class="warehouse-step-number">4</span>绑定明细</strong></div><div><el-button type="primary" size="small" @click="addTransferRelabelRow"><Plus :size="15" />添加 SKU 明细</el-button><el-button size="small" @click="clearTransferRelabelRows">清空</el-button></div></div>
|
||||
<div v-if="labelingMode === 'carton'" class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-transfer-mark-table"><thead><tr><th>客户箱唛号 *</th><th>旧 SKU</th><th>新 FBA 箱唛 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in transferRelabelRows" :key="row.id"><td><el-input v-model="row.customerCartonMarks" type="textarea" :rows="2" placeholder="请输入客户箱唛号" /></td><td><el-input :model-value="row.sku" readonly /></td><td><el-input v-model="row.fbaCartonMark" placeholder="请输入新的 FBA 箱唛" /></td><td><el-input v-model="row.remark" type="textarea" :rows="2" placeholder="请输入备注" /></td><td><button type="button" class="warehouse-delete" title="删除该明细" @click="transferRelabelRows = transferRelabelRows.filter((item) => item.id !== row.id)"><Trash2 :size="16" />删除</button></td></tr><tr v-if="transferRelabelRows.length === 0"><td colspan="5"><div class="warehouse-empty">暂无绑定明细,请点击“添加换标明细”</div></td></tr></tbody></table></div>
|
||||
<div v-else-if="labelingMode === 'sku'" class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-transfer-mark-table"><thead><tr><th>客户箱唛号 *</th><th>旧 SKU *</th><th>新 SKU *</th><th>换标数量(件)*</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in transferRelabelRows" :key="row.id"><td><el-input v-model="row.customerCartonMarks" type="textarea" :rows="2" placeholder="请输入客户箱唛号" /></td><td><el-input :model-value="row.sku" readonly /></td><td><el-input v-model="row.newSkuCode" placeholder="请输入新 SKU" /></td><td><el-input-number v-model="row.quantity" :min="1" :precision="0" controls-position="right" placeholder="请输入箱数" /></td><td><el-input v-model="row.remark" type="textarea" :rows="2" placeholder="请输入备注" /></td><td><button type="button" class="warehouse-delete" title="删除该明细" @click="transferRelabelRows = transferRelabelRows.filter((item) => item.id !== row.id)"><Trash2 :size="16" />删除</button></td></tr><tr v-if="transferRelabelRows.length === 0"><td colspan="6"><div class="warehouse-empty">暂无绑定明细,请点击“添加 SKU 明细”</div></td></tr></tbody></table></div>
|
||||
<div v-else class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-transfer-mark-table"><thead><tr><th>客户箱唛号 *</th><th>旧 SKU *</th><th>新 SKU *</th><th>换标数量(件)*</th><th>新 FBA 箱唛 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in transferRelabelRows" :key="row.id"><td><el-input v-model="row.customerCartonMarks" type="textarea" :rows="2" placeholder="请输入客户箱唛号" /></td><td><el-input :model-value="row.sku" readonly /></td><td><el-input v-model="row.newSkuCode" placeholder="请输入新 SKU" /></td><td><el-input-number v-model="row.quantity" :min="1" :precision="0" controls-position="right" placeholder="请输入数量" /></td><td><el-input v-model="row.fbaCartonMark" placeholder="请输入新的 FBA 箱唛" /></td><td><el-input v-model="row.remark" type="textarea" :rows="2" placeholder="请输入备注" /></td><td><button type="button" class="warehouse-delete" title="删除该明细" @click="transferRelabelRows = transferRelabelRows.filter((item) => item.id !== row.id)"><Trash2 :size="16" />删除</button></td></tr><tr v-if="transferRelabelRows.length === 0"><td colspan="7"><div class="warehouse-empty">暂无绑定明细,请点击“添加 SKU 明细”</div></td></tr></tbody></table></div>
|
||||
@@ -1137,9 +1349,13 @@ function selectRelabelInventory(inboundOrderId: string) {
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="warehouse-breadcrumb"><button type="button" @click="mode = 'list'">{{ config.title }}</button><b>/</b><strong>订单详情</strong></div>
|
||||
<div class="warehouse-page-heading"><h2>{{ config.title }}详情</h2><div><el-button v-if="canCancelDiscard && props.service === 'disposal' && selectedDetail?.status === 'PENDING_REVIEW'" type="danger" @click="cancelSelectedDiscard">取消申请</el-button><el-button v-if="canCreate && props.service === 'transfer' && selectedDetail?.status === 'DRAFT'" type="primary" @click="openEdit(String(selectedDetail?.transferNo || selectedDetail?.['0'] || ''))">编辑</el-button><el-button v-if="canConfirmTransfer && props.service === 'transfer' && selectedDetail?.status === 'WAIT_CUSTOMER_CONFIRMATION'" type="primary" @click="confirmTransferMeasurements">确认测量数据</el-button><el-button @click="mode = 'list'"><ArrowLeft :size="16" />返回列表</el-button></div></div>
|
||||
<div class="warehouse-card warehouse-form-section"><h3>订单概览</h3><div v-if="props.service === 'transfer'" class="warehouse-detail-grid"><div><span>转运单号</span><strong>{{ selectedDetail?.transferNo || '—' }}</strong></div><div><span>入库箱数</span><strong>{{ selectedDetail?.packageCount ?? '—' }}</strong></div><div><span>海外仓</span><strong>{{ warehouseDisplay(selectedDetail?.operatingWarehouseId) }}</strong></div><div><span>FBA 目的仓</span><strong>{{ selectedDetail?.destinationWarehouseName || warehouseDisplay(selectedDetail?.destinationWarehouseCode || selectedDetail?.fbaWarehouseCode) }}</strong></div><div><span>入仓方式</span><strong>{{ deliveryMethodLabel(selectedDetail?.deliveryMethod || '') }}</strong></div><div><span>是否换标</span><strong>{{ selectedDetail?.needRelabel ? '是' : '否' }}</strong></div><div><span>状态</span><strong>{{ statusLabel(selectedDetail?.status || '') }}</strong></div><div><span>创建时间</span><strong>{{ formatDateTime(selectedDetail?.createdAt) }}</strong></div></div><div v-else class="warehouse-detail-grid"><div v-for="(column, index) in config.columns.slice(1)" :key="column"><span>{{ column }}</span><strong>{{ selectedDetail?.[String(index)] || '—' }}</strong></div></div></div>
|
||||
<header class="warehouse-detail-header">
|
||||
<button type="button" class="warehouse-detail-back" @click="returnToList"><ArrowLeft :size="16" />返回列表</button>
|
||||
<div class="warehouse-detail-title"><strong>{{ props.service === 'labeling' ? selectedRelabelDetail?.application.relabelNo : selectedDetail?.transferNo || selectedDetail?.['0'] || `${config.title}详情` }}</strong><span>{{ config.title }}详情</span></div>
|
||||
<div class="warehouse-detail-actions"><el-button v-if="canCancelDiscard && props.service === 'disposal' && selectedDetail?.status === 'PENDING_REVIEW'" type="danger" @click="cancelSelectedDiscard">取消申请</el-button><el-button v-if="canCreate && props.service === 'transfer' && selectedDetail?.status === 'DRAFT'" type="primary" @click="openEdit(String(selectedDetail?.transferNo || selectedDetail?.['0'] || ''))">编辑</el-button><el-button v-if="canConfirmTransfer && props.service === 'transfer' && selectedDetail?.status === 'WAIT_CUSTOMER_CONFIRMATION'" type="primary" @click="confirmTransferMeasurements">确认测量数据</el-button><el-tag effect="light">{{ statusLabel(props.service === 'labeling' ? selectedRelabelDetail?.application.status || '' : String(selectedDetail?.status || '')) }}</el-tag></div>
|
||||
</header>
|
||||
<div v-if="detailError" class="warehouse-detail-error" role="alert"><strong>{{ detailError }}</strong><el-button @click="loadDetailPage">重新加载</el-button></div>
|
||||
<div v-else class="warehouse-card warehouse-form-section"><h3>订单概览</h3><div v-if="props.service === 'labeling'" class="warehouse-detail-grid"><div><span>操作仓</span><strong>{{ warehouseDisplay(selectedRelabelDetail?.application.overseasWarehouseId) }}</strong></div><div><span>客户参考号</span><strong>{{ selectedRelabelDetail?.application.customerOrderNo || '—' }}</strong></div><div><span>SKU 数</span><strong>{{ selectedRelabelDetail?.application.itemCount ?? '—' }}</strong></div><div><span>总箱数</span><strong>{{ selectedRelabelDetail?.application.totalPackageCount ?? '—' }}</strong></div><div><span>贴标截止时间</span><strong>{{ formatDateTime(selectedRelabelDetail?.application.labelDeadline) }}</strong></div><div><span>仓库联系人</span><strong>{{ selectedRelabelDetail?.application.contactName || '—' }}</strong></div><div><span>联系电话</span><strong>{{ selectedRelabelDetail?.application.contactPhone || '—' }}</strong></div><div><span>创建时间</span><strong>{{ formatDateTime(selectedRelabelDetail?.application.createdAt) }}</strong></div></div><div v-else class="warehouse-detail-grid"><div v-for="(column, index) in config.columns.slice(1)" :key="column"><span>{{ column }}</span><strong>{{ selectedDetail?.[String(index)] || '—' }}</strong></div></div></div>
|
||||
<div v-if="props.service === 'transfer'" class="warehouse-card warehouse-form-section warehouse-transfer-cargo-card">
|
||||
<div class="warehouse-section-heading"><h3>货物明细</h3><el-button v-if="selectedDetail?.status !== 'DRAFT'" native-type="button" type="primary" @click="printTransferCartons(String(selectedDetail?.transferNo || selectedDetail?.['0'] || ''))"><Printer :size="16" />打印箱唛</el-button></div>
|
||||
<div class="warehouse-table-scroll"><table class="warehouse-edit-table"><thead><tr><th>商品 SKU</th><th>中文品名</th><th>预计箱数</th><th>客户箱唛号</th></tr></thead><tbody><tr v-for="(item, index) in selectedDetail?.items || []" :key="`${item.skuCode}-${index}`"><td>{{ item.skuCode || '—' }}</td><td>{{ item.productNameZh || '—' }}</td><td>{{ item.packageCount ?? '—' }}</td><td>{{ item.shippingMark || '—' }}</td></tr><tr v-if="!selectedDetail?.items?.length"><td colspan="4"><div class="warehouse-empty">暂无货物明细</div></td></tr></tbody></table></div>
|
||||
@@ -1153,7 +1369,7 @@ function selectRelabelInventory(inboundOrderId: string) {
|
||||
<div class="warehouse-section-heading"><h3>换标完成与出库</h3></div>
|
||||
<div class="warehouse-detail-grid warehouse-fulfillment-grid">
|
||||
<div><span>派送方式</span><strong>{{ deliveryMethodLabel(selectedRelabelDetail?.application.deliveryMethod || '') || '无需出库' }}</strong></div>
|
||||
<div><span>履约状态</span><strong>{{ selectedRelabelDetail?.application.fulfillmentStatus || '—' }}</strong></div>
|
||||
<div><span>履约状态</span><strong>{{ statusLabel(selectedRelabelDetail?.application.fulfillmentStatus || '') }}</strong></div>
|
||||
<div><span>客户确认时间</span><strong>{{ formatDateTime(selectedRelabelDetail?.application.customerConfirmedAt) }}</strong></div>
|
||||
</div>
|
||||
<div class="warehouse-evidence"><span>仓库完工照片</span><div v-if="selectedRelabelDetail?.completionEvidence?.photoFileIds?.length" class="warehouse-photo-grid"><div v-for="(fileId, index) in selectedRelabelDetail.completionEvidence.photoFileIds" :key="fileId" class="warehouse-photo-item"><el-image v-if="completionPhotoUrls[fileId]" :src="completionPhotoUrls[fileId]" :preview-src-list="completionPhotoPreviewUrls" :initial-index="completionPhotoPreviewUrls.indexOf(completionPhotoUrls[fileId])" fit="cover" preview-teleported /><div v-else-if="completionPhotoLoading[fileId]" class="warehouse-photo-placeholder">照片 {{ index + 1 }} 加载中…</div><button v-else class="warehouse-photo-placeholder warehouse-photo-retry" type="button" @click="loadCompletionPhoto(fileId)">{{ completionPhotoErrors[fileId] ? `照片 ${index + 1} 加载失败,点击重试` : `加载照片 ${index + 1}` }}</button></div></div><strong v-else>仓库尚未上传完工照片</strong></div>
|
||||
@@ -1185,24 +1401,29 @@ function selectRelabelInventory(inboundOrderId: string) {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.warehouse-service-page{color:#25324a}.warehouse-breadcrumb{display:flex;align-items:center;gap:9px;margin:0 0 14px;color:#9aa3b2;font-size:12px}.warehouse-breadcrumb b{font-weight:400}.warehouse-breadcrumb strong{color:#606b7c}.warehouse-breadcrumb button{padding:0;border:0;background:transparent;color:#4376b8;cursor:pointer}.warehouse-page-heading{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}.warehouse-page-heading h2{margin:0;color:#17233a;font-size:22px;letter-spacing:0}.warehouse-card{margin-bottom:12px;border:1px solid #e7ebf1;border-radius:4px;background:#fff;box-shadow:0 1px 3px rgba(25,42,70,.04)}.warehouse-filter-card{padding:20px 22px 16px}.warehouse-filter-grid{display:grid;grid-template-columns:repeat(4,minmax(180px,1fr));gap:16px 20px}.warehouse-filter-grid label,.warehouse-form-grid label{display:flex;min-width:0;flex-direction:column;gap:7px}.warehouse-filter-grid label>span,.warehouse-form-grid label>span{color:#596579;font-size:12px}.warehouse-filter-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:18px}.warehouse-table-card{overflow:hidden}.warehouse-table-scroll{overflow-x:auto}table{width:100%;min-width:940px;border-collapse:collapse;font-size:12px}th{height:44px;padding:0 14px;border-bottom:1px solid #e8edf3;background:#f7f9fc;color:#4d5a6e;font-weight:650;text-align:left;white-space:nowrap}td{height:48px;padding:8px 14px;border-bottom:1px solid #eef1f5;color:#526176;white-space:nowrap}.warehouse-empty{display:grid;height:150px;color:#a0a9b7;place-items:center}.warehouse-link,.warehouse-delete{display:inline-flex;align-items:center;gap:5px;padding:4px;border:0;background:transparent;color:#2176d2;cursor:pointer}.warehouse-delete{color:#d64545}.warehouse-pagination{display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:14px 18px;color:#778398;font-size:12px}.warehouse-pagination span{margin-right:8px}.warehouse-page-number{width:28px;height:28px;border:1px solid #2176d2;border-radius:3px;background:#2176d2;color:#fff}.warehouse-form-section{padding:22px}.warehouse-form-section h3{margin:0 0 18px;padding-bottom:12px;border-bottom:1px solid #edf0f4;color:#26364f;font-size:15px;letter-spacing:0}.warehouse-form-grid{display:grid;grid-template-columns:repeat(2,minmax(240px,1fr));gap:18px 28px}.warehouse-form-grid i{margin-right:3px;color:#e24b4b;font-style:normal}.warehouse-span-2{grid-column:1/-1}.warehouse-section-heading{display:flex;align-items:center;justify-content:space-between}.warehouse-section-heading h3{flex:1}.warehouse-edit-table td{padding:9px 10px}.warehouse-edit-table :deep(.el-input-number){width:130px}.warehouse-hint{margin:12px 0 0;color:#8792a2;font-size:12px}.warehouse-submit-bar{display:flex;justify-content:center;gap:10px;padding:12px 0 30px}.warehouse-detail-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:24px}.warehouse-detail-grid div{display:flex;flex-direction:column;gap:7px}.warehouse-detail-grid span{color:#8a95a5;font-size:12px}.warehouse-detail-grid strong{color:#334157;font-size:13px}:deep(.el-button){border-radius:4px}:deep(.el-button span){display:inline-flex;align-items:center;gap:6px}:deep(.el-select),:deep(.el-date-editor.el-input){width:100%}@media(max-width:1100px){.warehouse-filter-grid{grid-template-columns:repeat(2,1fr)}.warehouse-detail-grid{grid-template-columns:repeat(2,1fr)}}
|
||||
.warehouse-service-page{display:grid;gap:12px;color:#25324a}.warehouse-breadcrumb{display:flex;align-items:center;gap:9px;margin:0 0 14px;color:#9aa3b2;font-size:12px}.warehouse-breadcrumb b{font-weight:400}.warehouse-breadcrumb strong{color:#606b7c}.warehouse-breadcrumb button{padding:0;border:0;background:transparent;color:#4376b8;cursor:pointer}.warehouse-page-heading{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}.warehouse-page-heading h2{margin:0;color:#17233a;font-size:22px;letter-spacing:0}.warehouse-card{margin-bottom:0;border:1px solid var(--client-border);border-radius:var(--client-radius);background:#fff;box-shadow:none}.warehouse-filter-card{padding:20px 22px 16px}.warehouse-filter-grid{display:grid;grid-template-columns:repeat(4,minmax(180px,1fr));gap:16px 20px}.warehouse-filter-grid label,.warehouse-form-grid label{display:flex;min-width:0;flex-direction:column;gap:7px}.warehouse-filter-grid label>span,.warehouse-form-grid label>span{color:#596579;font-size:12px}.warehouse-filter-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:18px}.warehouse-table-card{overflow:hidden}.warehouse-list-notice{padding:8px 12px;border-bottom:1px solid var(--client-border);background:#fff8e8;color:#8a6116;font-size:12px}.warehouse-table-scroll{overflow-x:auto}table{width:100%;min-width:940px;border-collapse:collapse;font-size:12px}th{height:44px;padding:0 14px;border-bottom:1px solid #e8edf3;background:#f7f9fc;color:#4d5a6e;font-weight:650;text-align:left;white-space:nowrap}td{height:48px;padding:8px 14px;border-bottom:1px solid #eef1f5;color:#526176;white-space:nowrap}.warehouse-empty{display:grid;height:72px;color:#a0a9b7;place-items:center}.warehouse-link,.warehouse-delete{display:inline-flex;align-items:center;gap:5px;padding:4px;border:0;background:transparent;color:#2176d2;cursor:pointer}.warehouse-delete{color:#d64545}.warehouse-pagination{display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:14px 18px;color:#778398;font-size:12px}.warehouse-pagination span{margin-right:8px}.warehouse-page-number{width:28px;height:28px;border:1px solid #2176d2;border-radius:3px;background:#2176d2;color:#fff}.warehouse-form-section{padding:22px}.warehouse-form-section h3{margin:0 0 18px;padding-bottom:12px;border-bottom:1px solid #edf0f4;color:#26364f;font-size:15px;letter-spacing:0}.warehouse-form-grid{display:grid;grid-template-columns:repeat(2,minmax(240px,1fr));gap:18px 28px}.warehouse-form-grid i{margin-right:3px;color:#e24b4b;font-style:normal}.warehouse-span-2{grid-column:1/-1}.warehouse-section-heading{display:flex;align-items:center;justify-content:space-between}.warehouse-section-heading h3{flex:1}.warehouse-edit-table td{padding:9px 10px}.warehouse-edit-table :deep(.el-input-number){width:130px}.warehouse-hint{margin:12px 0 0;color:#8792a2;font-size:12px}.warehouse-submit-bar{display:flex;justify-content:center;gap:10px;padding:12px 0 30px}.warehouse-detail-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:24px}.warehouse-detail-grid div{display:flex;flex-direction:column;gap:7px}.warehouse-detail-grid span{color:#8a95a5;font-size:12px}.warehouse-detail-grid strong{color:#334157;font-size:13px}:deep(.el-button){border-radius:4px}:deep(.el-button span){display:inline-flex;align-items:center;gap:6px}:deep(.el-select),:deep(.el-date-editor.el-input){width:100%}@media(max-width:1100px){.warehouse-filter-grid{grid-template-columns:repeat(2,1fr)}.warehouse-detail-grid{grid-template-columns:repeat(2,1fr)}}
|
||||
.warehouse-service-page--detail{display:block;overflow:hidden;border:1px solid var(--client-border);border-radius:var(--client-radius);background:#fff}.warehouse-detail-header{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:14px;min-height:64px;padding:10px 16px;border-bottom:1px solid var(--client-border)}.warehouse-detail-back{display:inline-flex;align-items:center;gap:5px;padding:5px 0;border:0;background:transparent;color:var(--el-color-primary);cursor:pointer}.warehouse-detail-title{display:flex;min-width:0;flex-direction:column;gap:3px}.warehouse-detail-title strong{overflow:hidden;color:#1f2d45;font-size:18px;text-overflow:ellipsis;white-space:nowrap}.warehouse-detail-title span{color:#7c899c;font-size:12px}.warehouse-detail-actions{display:flex;align-items:center;gap:8px}.warehouse-service-page--detail>.warehouse-card{border:0;border-top:1px solid var(--client-border);border-radius:0}.warehouse-service-page--detail>.warehouse-detail-header+.warehouse-card{border-top:0}.warehouse-service-page--detail>.warehouse-form-section{padding:0}.warehouse-service-page--detail .warehouse-form-section h3{min-height:40px;margin:0;padding:11px 16px 9px;border-bottom:1px solid #edf1f5;font-size:14px}.warehouse-service-page--detail .warehouse-detail-grid{grid-template-columns:repeat(4,minmax(0,1fr));gap:0;margin:0}.warehouse-service-page--detail .warehouse-detail-grid>div{min-height:48px;flex-direction:row;align-items:center;gap:10px;padding:8px 14px;border-right:1px solid #edf1f5;border-bottom:1px solid #edf1f5}.warehouse-service-page--detail .warehouse-detail-grid span{flex:0 0 90px}.warehouse-service-page--detail .warehouse-detail-grid strong{min-width:0;overflow-wrap:anywhere}.warehouse-service-page--detail .warehouse-fulfillment-grid{margin-bottom:0}.warehouse-service-page--detail .warehouse-evidence{margin:12px 14px}.warehouse-service-page--detail .warehouse-completion-remark,.warehouse-service-page--detail .warehouse-hint{margin:10px 14px}.warehouse-service-page--detail .warehouse-table-scroll table{margin:0}.warehouse-service-page--detail .warehouse-empty{height:64px}@media(max-width:1100px){.warehouse-service-page--detail .warehouse-detail-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:720px){.warehouse-detail-header{grid-template-columns:1fr auto}.warehouse-detail-back{grid-column:1/-1}.warehouse-detail-actions{justify-content:flex-end}}
|
||||
.warehouse-detail-error{display:flex;min-height:220px;align-items:center;justify-content:center;gap:12px;color:#b42318}
|
||||
.warehouse-checkbox-field{display:flex;min-height:32px;align-items:center;justify-content:flex-start;gap:16px}.warehouse-checkbox-field>span{color:#596579;font-size:12px}.warehouse-checkbox-field :deep(.el-checkbox){margin-right:0}
|
||||
.warehouse-choice-field{display:grid;grid-template-columns:150px auto 1fr;align-items:center;gap:14px;min-height:40px;padding:12px 14px;border:1px solid #e1e8f2;border-radius:5px;background:#f8faff}.warehouse-choice-field>span{color:#34445c;font-size:13px;font-weight:650}.warehouse-choice-field>span i{margin-right:3px;color:#e24b4b;font-style:normal}.warehouse-choice-field>small{color:#7a879a;font-size:12px}.warehouse-receiving-card{display:grid;grid-template-columns:2fr 1fr 1fr;gap:22px;padding:15px 16px;border:1px solid #cfe0f5;border-radius:5px;background:#f6faff}.warehouse-receiving-card div{display:flex;min-width:0;flex-direction:column;gap:6px}.warehouse-receiving-card span{color:#7c899c;font-size:12px}.warehouse-receiving-card strong{overflow-wrap:anywhere;color:#314561;font-size:13px;font-weight:600}
|
||||
.warehouse-hidden-input{display:none}.warehouse-label-table{min-width:1120px}.warehouse-attachment-tip{margin:10px 0 12px;color:#7d8797;font-size:12px}.warehouse-mark-mapping{display:flex;flex-wrap:wrap;gap:8px;margin:4px 0 8px;padding:12px;border:1px solid #dce7f5;border-radius:5px;background:#f7faff}.warehouse-mark-mapping strong{width:100%;color:#34445c}.warehouse-mark-mapping span{padding:5px 9px;border-radius:3px;background:#eaf2fc;color:#3e6593;font-size:12px}
|
||||
.warehouse-upload-summary{display:flex;flex-wrap:wrap;gap:8px 24px;margin:-4px 0 14px;padding:9px 12px;border:1px solid #dce7f5;border-radius:4px;background:#f8fbff;color:#596579;font-size:12px}
|
||||
.warehouse-upload-summary>span{display:inline-flex;align-items:center;gap:6px;overflow-wrap:anywhere}
|
||||
.warehouse-file-groups{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:16px;margin:0 0 18px}.warehouse-file-groups--single{grid-template-columns:minmax(0,1fr)}.warehouse-file-group{min-width:0;overflow:hidden;border:1px solid #dde5f0;border-radius:8px;background:#fbfcfe;box-shadow:0 1px 2px rgba(31,54,86,.04)}.warehouse-file-group--primary{border-color:#cbdcf2;background:linear-gradient(180deg,#f7faff 0,#fbfdff 100%)}.warehouse-file-group__heading{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;padding:14px 16px;border-bottom:1px solid #e7edf5;background:rgba(255,255,255,.68)}.warehouse-file-group__heading strong{display:flex;align-items:center;gap:8px;color:#2d405c;font-size:14px}.warehouse-file-group__heading strong>span{display:inline-grid;width:22px;height:22px;border-radius:50%;background:#3478c8;color:#fff;font-size:12px;place-items:center}.warehouse-file-group__heading em{flex-shrink:0;padding:4px 8px;border-radius:10px;background:#edf4fd;color:#4774aa;font-size:11px;font-style:normal}.warehouse-file-options{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:stretch;padding:14px}.warehouse-file-option{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:10px 12px;align-items:center;min-width:0;padding:12px;border:1px dashed #ced8e6;border-radius:6px;background:#fff}.warehouse-file-option.is-selected{border-style:solid;border-color:#79a8df;background:#f5f9ff}.warehouse-file-option__name strong{color:#34445a;font-size:13px}.warehouse-file-option__status{display:flex;grid-column:1/-1;align-items:center;justify-content:space-between;gap:8px;min-width:0;padding:7px 8px;border-radius:4px;background:#eaf3ff;color:#376da9;font-size:11px}.warehouse-file-option__status>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.warehouse-file-option__status.is-empty{background:#f4f6f8;color:#9aa4b2}.warehouse-file-choice-or{display:grid;color:#9ba6b5;font-size:11px;place-items:center}.warehouse-file-choice-or.is-both{color:#3478c8;font-weight:700}.warehouse-template-list{display:flex;flex-direction:column;padding:7px 14px}.warehouse-template-list>div{display:grid;grid-template-columns:minmax(150px,1fr) auto auto;align-items:center;gap:8px;padding:9px 0;border-bottom:1px solid #edf1f6}.warehouse-template-list>div:last-child{border-bottom:0}.warehouse-template-list strong{color:#3f4e63;font-size:12px}.warehouse-transfer-template-group{margin:0 0 14px}@media(max-width:1180px){.warehouse-file-groups{grid-template-columns:minmax(0,1fr)}}@media(max-width:720px){.warehouse-file-options{grid-template-columns:minmax(0,1fr)}.warehouse-file-choice-or{height:24px}.warehouse-template-list>div{grid-template-columns:minmax(0,1fr) auto}.warehouse-template-list>div>span{grid-column:1/-1}}
|
||||
.warehouse-file-clear{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:24px;height:24px;border:0;border-radius:4px;background:transparent;color:var(--muted);cursor:pointer}
|
||||
.warehouse-file-clear:hover:not(:disabled){color:#dc2626;background:#fee2e2}
|
||||
.warehouse-file-clear:disabled{opacity:.5;cursor:not-allowed}
|
||||
.warehouse-unmaintained{margin-left:8px;color:#e6a23c;font-size:12px;font-style:normal}
|
||||
.warehouse-labeling-tabs{margin-top:-8px}.warehouse-labeling-heading{align-items:flex-start}.warehouse-labeling-heading h3{margin-bottom:16px}.warehouse-template-actions{display:flex;gap:8px;padding-bottom:12px}.warehouse-tab-actions{align-items:center;margin:2px 0 14px}.warehouse-tab-actions p{margin:0;color:#7d8797;font-size:12px}.warehouse-tab-actions>div{display:flex;gap:8px}.warehouse-sku-notice{margin:0 0 14px;padding:10px 12px;border:1px solid #f2d39b;border-radius:4px;background:#fff8e8;color:#8a6116;font-size:12px}.warehouse-sku-label-table{min-width:980px}.warehouse-sku-label-table :deep(.el-input-number){width:160px}:deep(.warehouse-labeling-tabs>.el-tabs__header){margin-bottom:18px}:deep(.warehouse-labeling-tabs .el-tabs__item){height:42px;padding:0 26px;font-weight:600}.warehouse-labeling-mode{display:flex;align-items:center;gap:14px;margin:0 0 16px;padding:14px 16px;border:1px solid #dce5f5;border-radius:5px;background:#f7f9ff}.warehouse-labeling-mode>span{color:#34445c;font-size:13px;font-weight:650;white-space:nowrap}.warehouse-labeling-mode p{margin:0;color:#71809a;font-size:12px}.warehouse-labeling-mode :deep(.el-radio-group){display:flex;flex-shrink:0}.warehouse-labeling-mode :deep(.el-radio-button__inner){padding:8px 14px}
|
||||
.warehouse-relabel-detail-table{min-width:1420px}.warehouse-relabel-detail-table td strong{color:#253f67}
|
||||
.warehouse-service-page--return-relabel .warehouse-table-scroll:has(> .warehouse-return-relabel-table)~.warehouse-table-scroll{display:none}
|
||||
.warehouse-native-file{min-height:32px;padding:5px 8px;border:1px solid #dcdfe6;border-radius:4px;background:#fff;color:#606266}.warehouse-fulfillment-grid{margin-bottom:18px}.warehouse-evidence{display:flex;align-items:flex-start;gap:16px;padding:14px;border:1px solid #e7ebf1;border-radius:4px;background:#f8fafc}.warehouse-evidence>span{padding-top:6px;color:#71809a;font-size:12px;white-space:nowrap}.warehouse-evidence>strong{padding-top:6px;color:#8a95a5;font-size:12px;font-weight:400}.warehouse-photo-grid{display:grid;grid-template-columns:repeat(4,140px);gap:12px}.warehouse-photo-item{width:140px;height:105px;overflow:hidden;border:1px solid #dce3ed;border-radius:5px;background:#fff}.warehouse-photo-item :deep(.el-image){display:block;width:100%;height:100%;cursor:zoom-in}.warehouse-photo-placeholder{display:flex;width:100%;height:100%;align-items:center;justify-content:center;padding:10px;border:0;background:#f4f7fb;color:#8792a2;font-size:12px;text-align:center}.warehouse-photo-retry{cursor:pointer}.warehouse-completion-remark{margin:10px 0 0;color:#596579;font-size:12px}
|
||||
.warehouse-measurement-evidence{margin-top:16px}
|
||||
.warehouse-template-actions{flex-wrap:wrap;justify-content:flex-end}.warehouse-import-preview{margin:0 0 16px;padding:14px;border:1px solid #cfe0f5;border-radius:5px;background:#f8fbff}.warehouse-import-preview .warehouse-section-heading{margin-bottom:10px}.warehouse-import-preview .warehouse-section-heading>div{display:flex;align-items:center;gap:10px}.warehouse-import-preview p{margin:0;color:#7d8797;font-size:12px}.warehouse-binding-preview-table{min-width:980px;background:#fff}
|
||||
.warehouse-transfer-relabel .warehouse-labeling-heading>div:first-child{min-width:0}.warehouse-transfer-relabel .warehouse-labeling-heading h3{margin-bottom:6px}.warehouse-transfer-relabel .warehouse-labeling-heading p{margin:0 0 16px;color:#7d8797;font-size:12px}.warehouse-transfer-relabel-fields{margin-bottom:18px}.warehouse-transfer-mark-table{min-width:1420px}.warehouse-transfer-mark-table td{vertical-align:top}.warehouse-transfer-mark-table :deep(.el-textarea){min-width:220px}.warehouse-transfer-mark-table :deep(.el-input-number){width:150px}.warehouse-count-error{color:#d64545}
|
||||
.warehouse-transfer-form{display:grid;grid-template-columns:minmax(0,1fr);gap:12px}.warehouse-transfer-form--relabel{grid-template-columns:minmax(0,1fr) minmax(0,1.35fr);align-items:start}.warehouse-transfer-form--relabel .warehouse-transfer-main-card{grid-column:1;grid-row:1}.warehouse-transfer-form--relabel .warehouse-transfer-cargo-card{grid-column:1;grid-row:2}.warehouse-transfer-form--relabel .warehouse-transfer-relabel{grid-column:2;grid-row:1 / span 2;margin-bottom:0}.warehouse-transfer-form--relabel .warehouse-transfer-submit-bar{grid-column:1 / -1}.warehouse-transfer-form :deep(.el-input__wrapper),.warehouse-transfer-form :deep(.el-textarea__inner),.warehouse-transfer-form :deep(.el-select__wrapper),.warehouse-transfer-form :deep(.el-date-editor.el-input__wrapper){min-height:32px}.warehouse-transfer-form .warehouse-form-section{padding:16px}.warehouse-transfer-form .warehouse-form-section h3{margin-bottom:12px;padding-bottom:9px;font-size:14px}.warehouse-transfer-form .warehouse-form-grid{gap:12px 16px}.warehouse-transfer-form .warehouse-choice-field{min-height:34px;padding:8px 10px;grid-template-columns:118px auto 1fr;gap:10px}.warehouse-transfer-form .warehouse-choice-field>span{font-size:12px}.warehouse-transfer-form .warehouse-choice-field small{font-size:11px}.warehouse-transfer-form .warehouse-receiving-card{gap:12px;padding:10px 12px}.warehouse-transfer-form .warehouse-section-heading{margin-bottom:8px}.warehouse-transfer-form .warehouse-edit-table{font-size:11px}.warehouse-transfer-form .warehouse-edit-table th{height:36px;padding:0 9px}.warehouse-transfer-form .warehouse-edit-table td{height:42px;padding:6px 7px}.warehouse-transfer-form .warehouse-transfer-mark-table :deep(.el-textarea){min-width:150px}.warehouse-transfer-form .warehouse-transfer-mark-table :deep(.el-input-number){width:112px}.warehouse-transfer-form .warehouse-hint{margin-top:5px;font-size:11px}.warehouse-transfer-form .warehouse-labeling-mode{gap:9px;margin-bottom:10px;padding:9px 10px}.warehouse-transfer-form .warehouse-labeling-mode>span{font-size:12px}.warehouse-transfer-form .warehouse-labeling-mode p{font-size:11px}.warehouse-transfer-form .warehouse-labeling-mode :deep(.el-radio-button__inner){padding:6px 9px}.warehouse-transfer-form .warehouse-template-actions{gap:5px;padding-bottom:8px}.warehouse-transfer-form .warehouse-template-actions :deep(.el-button){padding:6px 8px;font-size:11px}.warehouse-transfer-form .warehouse-submit-bar{padding:4px 0 18px}.warehouse-transfer-form .warehouse-submit-bar :deep(.el-button){min-height:32px;padding:7px 14px}.warehouse-transfer-form .warehouse-transfer-relabel{overflow:hidden}.warehouse-transfer-form .warehouse-transfer-relabel-fields{margin-bottom:10px}
|
||||
.warehouse-transfer-relabel .warehouse-labeling-heading>div:first-child{min-width:0}.warehouse-transfer-relabel .warehouse-labeling-heading h3{margin-bottom:6px}.warehouse-transfer-relabel .warehouse-labeling-heading p{margin:0 0 16px;color:#7d8797;font-size:12px}.warehouse-transfer-relabel-fields{margin-bottom:18px}.warehouse-transfer-file-groups{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(360px,.85fr);align-items:stretch;gap:14px;margin-bottom:14px}.warehouse-transfer-total-files,.warehouse-transfer-template-group{height:100%;margin:0}.warehouse-transfer-mark-table{min-width:1420px}.warehouse-transfer-mark-table td{vertical-align:top}.warehouse-transfer-mark-table :deep(.el-textarea){min-width:220px}.warehouse-transfer-mark-table :deep(.el-input-number){width:150px}.warehouse-count-error{color:#d64545}
|
||||
.warehouse-transfer-form{display:grid;grid-template-columns:minmax(0,1fr);gap:12px}.warehouse-transfer-form--relabel{grid-template-columns:minmax(0,1fr) minmax(0,1.35fr);align-items:start}.warehouse-transfer-form--relabel .warehouse-transfer-main-card{grid-column:1;grid-row:1}.warehouse-transfer-form--relabel .warehouse-transfer-cargo-card{grid-column:1;grid-row:2}.warehouse-transfer-form--relabel .warehouse-transfer-relabel{grid-column:2;grid-row:1 / span 2;margin-bottom:0}.warehouse-transfer-form--relabel .warehouse-transfer-submit-bar{grid-column:1 / -1}.warehouse-transfer-form :deep(.el-input__wrapper),.warehouse-transfer-form :deep(.el-textarea__inner),.warehouse-transfer-form :deep(.el-select__wrapper),.warehouse-transfer-form :deep(.el-date-editor.el-input__wrapper){min-height:32px}.warehouse-transfer-form .warehouse-form-section{padding:16px}.warehouse-transfer-form .warehouse-form-section h3{margin-bottom:12px;padding-bottom:9px;font-size:14px}.warehouse-transfer-form .warehouse-form-grid{gap:12px 16px}.warehouse-transfer-form .warehouse-choice-field{min-height:34px;padding:8px 10px;grid-template-columns:118px minmax(0,1fr);gap:10px}.warehouse-transfer-form .warehouse-choice-field>span{font-size:12px}.warehouse-transfer-form .warehouse-receiving-card{gap:12px;padding:10px 12px}.warehouse-transfer-form .warehouse-section-heading{margin-bottom:8px}.warehouse-transfer-form .warehouse-edit-table{font-size:11px}.warehouse-transfer-form .warehouse-edit-table th{height:36px;padding:0 9px}.warehouse-transfer-form .warehouse-edit-table td{height:42px;padding:6px 7px}.warehouse-transfer-form .warehouse-transfer-mark-table :deep(.el-textarea){min-width:150px}.warehouse-transfer-form .warehouse-transfer-mark-table :deep(.el-input-number){width:112px}.warehouse-transfer-form .warehouse-hint{margin-top:5px;font-size:11px}.warehouse-transfer-form .warehouse-labeling-mode{gap:9px;margin-bottom:10px;padding:9px 10px}.warehouse-transfer-form .warehouse-labeling-mode>span{font-size:12px}.warehouse-transfer-form .warehouse-labeling-mode :deep(.el-radio-button__inner){padding:6px 9px}.warehouse-transfer-form .warehouse-template-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:5px;padding-bottom:8px}.warehouse-transfer-form .warehouse-template-actions :deep(.el-button){padding:6px 8px;font-size:11px}.warehouse-transfer-form .warehouse-submit-bar{padding:4px 0 18px}.warehouse-transfer-form .warehouse-submit-bar :deep(.el-button){min-height:32px;padding:7px 14px}.warehouse-transfer-form .warehouse-transfer-relabel{overflow:hidden}.warehouse-transfer-form .warehouse-transfer-relabel-fields{margin-bottom:10px}.warehouse-step-heading{align-items:flex-start}.warehouse-step-heading h3{display:flex;align-items:center;gap:9px}.warehouse-step-number{display:inline-grid;flex:0 0 auto;width:24px;height:24px;border-radius:50%;background:#3478c8;color:#fff;font-size:12px;place-items:center}.warehouse-heading-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px}.warehouse-transfer-template-group{margin:0 0 14px;padding:12px 14px;border:1px solid #dde5f0;border-radius:7px;background:#fbfcfe}.warehouse-binding-heading{margin-top:14px;padding:12px 14px;border:1px solid #dde5f0;border-radius:7px;background:#f8faff}.warehouse-binding-heading strong{display:flex;align-items:center;gap:8px;color:#34445c;font-size:13px}
|
||||
@media(max-width:1250px){.warehouse-transfer-form--relabel{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.warehouse-transfer-form .warehouse-template-actions{justify-content:flex-start}.warehouse-transfer-form .warehouse-labeling-heading{align-items:flex-start;flex-direction:column}}
|
||||
@media(max-width:1180px){.warehouse-transfer-file-groups{grid-template-columns:minmax(0,1fr)}}
|
||||
.warehouse-transfer-form .pickup-address-field{grid-column:span 1}
|
||||
/* Keep the count and controls visible together on narrow list pages. */
|
||||
.warehouse-pagination { justify-content: flex-end; }
|
||||
|
||||
@@ -17,11 +17,32 @@ test('总文件可以独立清除并重选同名文件,提交时禁止清除',
|
||||
assert.match(view, /aria-label="清除 SKU 总文件" :disabled="saving"/)
|
||||
})
|
||||
|
||||
test('贴标申请从当前客户仍有库存的历史入仓单选择来源', () => {
|
||||
test('贴标文件按总文件和关联模板分组展示', () => {
|
||||
assert.match(view, /warehouse-file-groups/)
|
||||
assert.match(view, /<span>1<\/span>贴标总文件/)
|
||||
assert.match(view, /必传 · 二选一/)
|
||||
assert.match(view, /必传 · 两份文件/)
|
||||
assert.match(view, /<span>2<\/span>明细关联模板/)
|
||||
assert.match(view, /SKU 关联/)
|
||||
assert.match(view, /箱号关联/)
|
||||
assert.match(view, /SKU 混箱/)
|
||||
assert.match(view, /warehouse-file-groups--single/)
|
||||
})
|
||||
|
||||
test('贴标申请移除操作项下方的解释文字,保留必要状态', () => {
|
||||
assert.doesNotMatch(view, /选择后仅查询当前客户在该中转仓的可用库存/)
|
||||
assert.doesNotMatch(view, /托盘仅作为库内承载位置/)
|
||||
assert.doesNotMatch(view, /整张贴标申请只能选择一个目的仓/)
|
||||
assert.doesNotMatch(view, /SKU 与箱号关联通过仓库唛头合并/)
|
||||
assert.match(view, /必传 · 二选一/)
|
||||
assert.match(view, /必传 · 两份文件/)
|
||||
})
|
||||
|
||||
test('贴标申请可从当前客户仍有库存的入仓单或退货单选择来源', () => {
|
||||
assert.match(view, /海外操作仓(中转仓)/)
|
||||
assert.match(view, /选择有库存的历史入仓单/)
|
||||
assert.match(view, /曾经下单且当前仍有库存的入仓单/)
|
||||
assert.match(view, /当前中转仓暂无已入库可用库存/)
|
||||
assert.match(view, /选择有库存的入仓单或退货单/)
|
||||
assert.match(view, /quantityUnit: candidate\.sourceType === 'RETURN_CARTON' \? '件' : '箱'/)
|
||||
assert.match(view, /当前中转仓暂无可用库存/)
|
||||
assert.match(view, /relabelInboundOptions/)
|
||||
assert.match(view, /listRelabelInventoryCandidates\(form\.warehouse\)/)
|
||||
assert.match(api, /relabel-applications\/inventory-candidates/)
|
||||
@@ -38,12 +59,11 @@ test('客户贴标表单不展示客户单号、托盘和内部库存来源列',
|
||||
|
||||
test('贴标申请保留关联模板下载和上传入口', () => {
|
||||
assert.match(view, /downloadRelabelTemplate/)
|
||||
assert.match(view, /下载 SKU 关联模板/)
|
||||
assert.match(view, /上传 SKU 关联/)
|
||||
assert.match(view, /下载箱号关联模板/)
|
||||
assert.match(view, /上传箱号关联/)
|
||||
assert.match(view, /下载 SKU 混箱模板/)
|
||||
assert.match(view, /上传 SKU 混箱/)
|
||||
assert.match(view, /<strong>SKU 关联<\/strong>/)
|
||||
assert.match(view, /<strong>箱号关联<\/strong>/)
|
||||
assert.match(view, /<strong>SKU 混箱<\/strong>/)
|
||||
assert.match(view, />下载模板<\/el-button>/)
|
||||
assert.match(view, />上传文件<\/el-button>/)
|
||||
assert.match(view, /skuTemplateInput\?\.click\(\)/)
|
||||
assert.match(view, /cartonTemplateInput\?\.click\(\)/)
|
||||
assert.match(view, /importRelabelTemplate/)
|
||||
@@ -51,7 +71,7 @@ test('贴标申请保留关联模板下载和上传入口', () => {
|
||||
assert.match(view, /'明细行标识', '旧箱唛', '旧SKU标签', '新SKU标签'/)
|
||||
assert.match(view, /const includeDestination = false/)
|
||||
assert.doesNotMatch(view, /const includeDestination = props\.service !== 'transfer'/)
|
||||
assert.match(view, /SKU 与箱号关联通过仓库唛头合并/)
|
||||
assert.match(view, /<strong>绑定明细<\/strong>/)
|
||||
assert.match(view, /<th>仓库唛头 \*<\/th>/)
|
||||
assert.doesNotMatch(view, /库存位置ID/)
|
||||
})
|
||||
@@ -76,7 +96,7 @@ test('上传关系支持部分或全量选择后导入本次换标明细', () =>
|
||||
})
|
||||
|
||||
test('箱唛换标一行固定代表一箱', () => {
|
||||
const cartonTable = view.match(/<div v-if="labelingMode === 'carton'" class="warehouse-table-scroll">.+?(?=<div v-else class="warehouse-table-scroll">)/s)?.[0]
|
||||
const cartonTable = view.match(/<div v-if="!isReturnRelabelSource && labelingMode === 'carton'" class="warehouse-table-scroll">.+?(?=<div v-else class="warehouse-table-scroll">)/s)?.[0]
|
||||
assert.ok(cartonTable)
|
||||
assert.match(cartonTable, /<th>箱数<\/th>/)
|
||||
assert.match(cartonTable, /<td><strong>1 箱<\/strong><\/td>/)
|
||||
@@ -115,22 +135,47 @@ test('箱唛相关换标必须选择四选一派送方式', () => {
|
||||
assert.match(view, /labelingMode\.value !== 'sku' && !form\.deliveryMethod/)
|
||||
})
|
||||
|
||||
test('一次申请单选库存来源且保留普通入仓或退货合箱归属', () => {
|
||||
test('一次申请单选库存来源且退货库存按退货单汇总', () => {
|
||||
assert.doesNotMatch(view, /multiple filterable collapse-tags/)
|
||||
assert.match(view, /RETURN_CARTON:\$\{item\.returnCartonId\}/)
|
||||
assert.match(view, /RETURN_ORDER:\$\{returnOrderNo\}/)
|
||||
assert.match(view, /current\.availablePackages \+= inventoryQuantity\(candidate\.availablePackages\)/)
|
||||
assert.match(view, /availablePackages: inventoryQuantity\(candidate\.availablePackages\)/)
|
||||
assert.match(view, /const quantity = Number\(value\)/)
|
||||
assert.match(view, /interface ReturnRelabelSkuRow/)
|
||||
assert.match(view, /current\.availableQuantity \+= quantity/)
|
||||
assert.match(view, /库存总数/)
|
||||
assert.match(view, /本次贴标数量/)
|
||||
assert.match(view, /returnRelabelSkuRows\.value = \[\.\.\.grouped\.values\(\)\]/)
|
||||
assert.match(view, /createReturnRelabel\(selectedReturnOrderNo\.value/)
|
||||
assert.match(view, /uploadReturnRelabelFile\(created\.relabelNo, fileScope, selectedFile\)/)
|
||||
assert.doesNotMatch(view, /candidate\.sourceType === 'RETURN_CARTON'\) rows\.push\(\.\.\.Array\.from/)
|
||||
assert.match(view, /sourceInventoryBalanceId: String\(candidate\.inventoryBalanceId\)/)
|
||||
assert.match(view, /sourceInventoryPositionId: String\(position\.positionId\)/)
|
||||
assert.match(view, /sourceInboundOrderId: String\(candidate\.inboundOrderId\)/)
|
||||
assert.match(view, /returnCartonId: String\(candidate\.returnCartonId\)/)
|
||||
assert.match(api, /sourceInventoryBalanceId\?: string; sourceInventoryPositionId\?: string; sourceInboundOrderId\?: string; returnCartonId\?: string/)
|
||||
})
|
||||
|
||||
test('退货贴标总文件二选一且提交使用退货专用链路', () => {
|
||||
assert.match(view, /cartonTotalFile\.value && !requiresBothTotalFiles\.value\) clearCreateTotalFile\('SKU_TOTAL'\)/)
|
||||
assert.match(view, /skuTotalFile\.value && !requiresBothTotalFiles\.value\) clearCreateTotalFile\('CARTON_TOTAL'\)/)
|
||||
assert.match(view, /fileScope = cartonTotalFile\.value \? 'CARTON_TOTAL' as const : 'SKU_TOTAL' as const/)
|
||||
assert.match(view, /destinationFbaWarehouseId: String\(destination\.id\)/)
|
||||
})
|
||||
|
||||
test('SKU 和箱唛都换时两份总文件必须同时上传', () => {
|
||||
assert.match(view, /labelingMode\.value === 'carton_and_sku'/)
|
||||
assert.match(view, /requiresBothTotalFiles\.value && \(!cartonTotalFile\.value \|\| !skuTotalFile\.value\)/)
|
||||
assert.match(view, /必须同时上传箱唛总文件和 SKU 总文件/)
|
||||
assert.match(view, /requiresBothTotalFiles \? '和' : '或'/)
|
||||
assert.match(view, /必传 · 两份文件/)
|
||||
})
|
||||
|
||||
test('海外操作仓和FBA目的仓使用独立字段', () => {
|
||||
assert.match(api, /operationWarehouseId: string/)
|
||||
assert.match(api, /destinationFbaWarehouseCode\?: string/)
|
||||
assert.match(view, /operationWarehouseId: form\.warehouse/)
|
||||
assert.match(view, /destinationFbaWarehouseCode: form\.destination/)
|
||||
assert.match(view, /整张贴标申请只能选择一个目的仓/)
|
||||
assert.match(view, /<el-select v-model="form\.destination"[^\n]+FBA 目的仓/)
|
||||
assert.doesNotMatch(view, /v-model="row\.destinationWarehouseId"[^\n]+请选择 FBA 目的仓/)
|
||||
})
|
||||
|
||||
@@ -164,8 +209,8 @@ test('贴标详情展示箱唛、SKU 子明细和独立FBA目的仓', () => {
|
||||
})
|
||||
|
||||
test('客户在换标列表确认完工且照片使用二进制下载', () => {
|
||||
assert.match(view, /row\.__status === 'CUSTOMER_CONFIRM_PENDING'/)
|
||||
assert.match(view, /confirmRelabelFromList\(row\['0'\]\)/)
|
||||
assert.match(view, /item\.__status === 'CUSTOMER_CONFIRM_PENDING'/)
|
||||
assert.match(view, /confirmRelabelFromList\(item\['0'\]\)/)
|
||||
assert.doesNotMatch(view, /selectedRelabelDetail\?\.application\.status === 'CUSTOMER_CONFIRM_PENDING'/)
|
||||
assert.match(api, /overseasBlobRequest/)
|
||||
})
|
||||
@@ -179,5 +224,6 @@ test('客户详情进入页面后直接回显完工照片并支持大图预览',
|
||||
})
|
||||
|
||||
test('进入复核后客户订单回显换标完成状态', () => {
|
||||
assert.match(view, /\['CUSTOMER_CONFIRM_PENDING', 'READY_FOR_OUTBOUND', 'COMPLETED'\]\.includes\(value\) \? '换标完成'/)
|
||||
assert.match(view, /value === 'CUSTOMER_CONFIRM_PENDING'\) return '待客户确认'/)
|
||||
assert.match(view, /\['READY_FOR_OUTBOUND', 'COMPLETED'\]\.includes\(value\) \? '换标完成'/)
|
||||
})
|
||||
|
||||
@@ -5,6 +5,12 @@ import test from 'node:test'
|
||||
const view = readFileSync(new URL('./WarehouseServiceView.vue', import.meta.url), 'utf8')
|
||||
const api = readFileSync(new URL('../api/warehouseServices.ts', import.meta.url), 'utf8')
|
||||
|
||||
test('贴标总文件与关联模板保持左右分组,退货来源使用专用明细', () => {
|
||||
assert.match(view, /class="warehouse-file-groups" :class="\{ 'warehouse-file-groups--single': isReturnRelabelSource \}"/)
|
||||
assert.match(view, /warehouse-service-page--return-relabel/)
|
||||
assert.match(view, /const sourceValue = `RETURN_ORDER:\$\{requestedReturnNo\}`/)
|
||||
})
|
||||
|
||||
test('仓储转运先选择入仓方式并按方式切换地址信息', () => {
|
||||
assert.match(view, /value="WAREHOUSE_PICKUP">仓库上门提货/)
|
||||
assert.match(view, /value="CUSTOMER_DELIVERY">客户送货到仓/)
|
||||
@@ -14,14 +20,14 @@ test('仓储转运先选择入仓方式并按方式切换地址信息', () => {
|
||||
})
|
||||
|
||||
test('仓储转运支持无目的地、FBA 仓和私人仓并显式选择是否换标', () => {
|
||||
assert.match(view, /是否有目的去向/)
|
||||
assert.match(view, />目的去向</)
|
||||
assert.match(view, /value="NONE">暂未确定/)
|
||||
assert.match(view, /value="FBA">FBA 仓/)
|
||||
assert.match(view, /value="PRIVATE">私人仓/)
|
||||
assert.match(view, /transferDestinationType === 'PRIVATE'.*私人仓地址.*邮编.*私人仓联系人.*联系电话/s)
|
||||
assert.match(view, /是否需要换标/)
|
||||
assert.match(view, /:value="false">不需要换标/)
|
||||
assert.match(view, /:value="true">需要换标/)
|
||||
assert.match(view, />换标服务</)
|
||||
assert.match(view, /:value="false">不需要/)
|
||||
assert.match(view, /:value="true">需要/)
|
||||
assert.match(api, /fbaWarehouseCode\?: string/)
|
||||
assert.match(api, /privateDestinationAddress\?: string; privateDestinationPostalCode\?: string/)
|
||||
assert.match(api, /fbaShipmentNo\?: string/)
|
||||
@@ -30,13 +36,11 @@ test('仓储转运支持无目的地、FBA 仓和私人仓并显式选择是否
|
||||
})
|
||||
|
||||
test('仓储转运换标整票只允许一个目的仓', () => {
|
||||
assert.match(view, /props\.service === 'transfer' && transferDestinationType === 'FBA'[^\n]+未换标货物目的 FBA 仓/)
|
||||
assert.match(view, /此处只表示不换标箱子的去向/)
|
||||
assert.match(view, /props\.service === 'transfer' && transferDestinationType === 'FBA'[^\n]+FBA 目的仓/)
|
||||
assert.match(view, /FBA 货件编号<\/span><el-input v-model="form\.fbaShipmentNo" placeholder="选填,可后续补充"/)
|
||||
assert.match(view, /transferDestinationType\.value === 'FBA' && !form\.destination/)
|
||||
assert.match(view, /transferDestinationType\.value === 'PRIVATE'.*!form\.privateDestinationPostalCode/)
|
||||
assert.match(view, /整票换标货物目的 FBA 仓/)
|
||||
assert.match(view, /暂不支持一票多个目的仓/)
|
||||
assert.match(view, /if \(!transferRelabelDefaultDestination\.value\) \{ ElMessage\.warning\('请选择整票换标货物的 FBA 目的仓'\)/)
|
||||
assert.match(view, /destinationWarehouseCode: transferRelabelDefaultDestination\.value \|\| undefined/)
|
||||
const transferRelabelSection = view.split('warehouse-transfer-relabel">')[1]?.split('warehouse-transfer-submit-bar')[0] || ''
|
||||
@@ -56,15 +60,14 @@ test('仓储转运不要求客户填写内部入仓号和我方箱唛,但换
|
||||
assert.doesNotMatch(transferConfig, /进仓编号/)
|
||||
assert.doesNotMatch(transferSubmit, /inboundCode|sourceLastlegOrderIds/)
|
||||
assert.match(view, /货物明细/)
|
||||
assert.match(view, /客户只填写自己的箱唛,我方箱唛由系统生成/)
|
||||
assert.match(view, /客户箱唛号 \*/)
|
||||
assert.match(view, /downloadTransferCartonMarkTemplate/)
|
||||
assert.match(view, /importTransferCartonMarks/)
|
||||
assert.match(view, /下载 SKU 关联模板/)
|
||||
assert.match(view, /上传 SKU 关联/)
|
||||
assert.match(view, /下载箱号关联模板/)
|
||||
assert.match(view, /上传箱号关联/)
|
||||
assert.match(view, /下载 SKU 混箱模板/)
|
||||
assert.match(view, /上传 SKU 混箱/)
|
||||
assert.match(view, /<strong>SKU 关联<\/strong>/)
|
||||
assert.match(view, /<strong>箱号关联<\/strong>/)
|
||||
assert.match(view, /<strong>SKU 混箱<\/strong>/)
|
||||
assert.match(view, />下载模板<\/el-button>/)
|
||||
assert.match(view, />上传文件<\/el-button>/)
|
||||
assert.match(view, /换标类型/)
|
||||
assert.match(view, /v-if="labelingMode === 'carton'"/)
|
||||
assert.match(view, /v-else-if="labelingMode === 'sku'"/)
|
||||
@@ -80,9 +83,50 @@ test('仓储转运不要求客户填写内部入仓号和我方箱唛,但换
|
||||
assert.doesNotMatch(view, /<th v-if="props\.service === 'transfer'">FNSKU \/ 箱唛<\/th>/)
|
||||
})
|
||||
|
||||
test('仓储转运货物明细可按 SKU 和箱数打印客户预贴箱唛', () => {
|
||||
assert.match(view, /printTransferDraftCartonMarks/)
|
||||
assert.match(view, /请先填写商品 SKU 和对应的整数箱数/)
|
||||
assert.match(view, /Array\.from\(\{ length: Number\(row\.quantity\) \}/)
|
||||
assert.match(view, /row\.customerCartonMarks = marks\.join\('\\n'\)/)
|
||||
assert.match(view, /仓储转运客户箱唛/)
|
||||
assert.match(view, /@click="printTransferDraftCartonMarks"/)
|
||||
assert.match(view, /打印唛头/)
|
||||
assert.match(view, /单次最多打印 500 张箱唛/)
|
||||
})
|
||||
|
||||
test('仓储转运换标资料按步骤分组展示', () => {
|
||||
assert.match(view, /<span v-if="props\.service === 'transfer'" class="warehouse-step-number">1<\/span>货物明细/)
|
||||
assert.match(view, /<span class="warehouse-step-number">2<\/span>换标申请/)
|
||||
assert.match(view, /<strong><span>3<\/span>明细关联模板<\/strong>/)
|
||||
assert.match(view, /<span class="warehouse-step-number">4<\/span>绑定明细/)
|
||||
assert.match(view, /warehouse-transfer-template-group/)
|
||||
assert.match(view, /warehouse-transfer-relabel/)
|
||||
})
|
||||
|
||||
test('仓储转运需要换标时上传总文件并挂到关联换标单', () => {
|
||||
const transferRelabelSection = view.split('warehouse-transfer-relabel">')[1]?.split('warehouse-transfer-submit-bar')[0] || ''
|
||||
assert.match(transferRelabelSection, /warehouse-transfer-total-files/)
|
||||
assert.match(transferRelabelSection, />贴标总文件</)
|
||||
assert.match(transferRelabelSection, />箱唛总文件</)
|
||||
assert.match(transferRelabelSection, />SKU 总文件</)
|
||||
assert.match(view, /action === 'submit' && requiresBothTotalFiles\.value/)
|
||||
assert.match(view, /const submitted = await submitTransfer\(transferNo\)/)
|
||||
assert.match(view, /submitted\.relabelNo \|\| created\.relabelNo/)
|
||||
assert.match(view, /uploadRelabelFile\(relabelNo, 'CARTON_TOTAL', cartonTotalFile\.value\)/)
|
||||
assert.match(view, /uploadRelabelFile\(relabelNo, 'SKU_TOTAL', skuTotalFile\.value\)/)
|
||||
assert.match(view, /转运单已提交,附件待补传/)
|
||||
})
|
||||
|
||||
test('仓储转运换标总文件与明细模板在桌面端左右排列', () => {
|
||||
assert.match(view, /class="warehouse-transfer-file-groups"/)
|
||||
assert.match(view, /\.warehouse-transfer-file-groups\{display:grid;grid-template-columns:minmax\(0,1\.15fr\) minmax\(360px,\.85fr\)/)
|
||||
assert.match(view, /\.warehouse-transfer-total-files,\.warehouse-transfer-template-group\{height:100%;margin:0\}/)
|
||||
assert.match(view, /@media\(max-width:1180px\)\{\.warehouse-transfer-file-groups\{grid-template-columns:minmax\(0,1fr\)\}\}/)
|
||||
})
|
||||
|
||||
test('上门提货不提交物流号,需要换标时随转运单登记申请', () => {
|
||||
assert.match(view, /trackingNo:\s*form\.deliveryMethod === 'CUSTOMER_DELIVERY'\s*\?[^:]+:\s*undefined/)
|
||||
assert.match(view, /本次提交将同时登记换标申请/)
|
||||
assert.match(view, /relabelItems: form\.relabel/)
|
||||
assert.match(view, /form\.deliveryMethod === 'CUSTOMER_DELIVERY'.*物流跟踪号/s)
|
||||
assert.match(view, /提货联系人/)
|
||||
assert.match(view, /联系人电话/)
|
||||
@@ -102,14 +146,14 @@ test('仓储转运列表操作仓使用海外仓接口数据', () => {
|
||||
assert.match(view, /loadAllWarehouseRecords\('OVERSEAS', getMerchantWarehouses\)/)
|
||||
assert.match(view, /warehouseDisplay\(item\.operatingWarehouseId\)/)
|
||||
assert.match(view, /\['OVERSEAS', 'OVERSEAS_PARTNER'\]/)
|
||||
assert.match(view, /v-for="warehouse in \(props\.service === 'relabel' \? fbaWarehouses : operationWarehouses\)"/)
|
||||
assert.match(view, /props\.service === 'relabel' \? fbaWarehouses\.value : operationWarehouses\.value/)
|
||||
assert.doesNotMatch(view, /美西洛杉矶仓/)
|
||||
})
|
||||
|
||||
test('仓储转运预计发货时间可选并只读展示实际发货时间', () => {
|
||||
assert.match(view, /预计发货时间/)
|
||||
assert.match(view, /v-model="form\.estimatedShipAt"/)
|
||||
assert.match(view, /选填,可线下确认后补充/)
|
||||
assert.match(view, /v-model="form\.estimatedShipAt"[^>]+placeholder="选填"/)
|
||||
assert.doesNotMatch(view, /!form\.estimatedShipAt && '预计发货时间'/)
|
||||
assert.match(view, /estimatedShipAt: form\.estimatedShipAt/)
|
||||
assert.match(view, /实际发货时间由仓库出库或 PDA 完成后回填/)
|
||||
@@ -140,8 +184,8 @@ test('仓储转运详情按接口具名字段展示订单概览', () => {
|
||||
})
|
||||
|
||||
test('PDA 打托完成后客户可从列表或详情确认并查看实测数据', () => {
|
||||
assert.match(view, /row\.__status === 'WAIT_CUSTOMER_CONFIRMATION'/)
|
||||
assert.match(view, /confirmTransferFromList\(row\['0'\]\)/)
|
||||
assert.match(view, /item\.__status === 'WAIT_CUSTOMER_CONFIRMATION'/)
|
||||
assert.match(view, /confirmTransferFromList\(item\['0'\]\)/)
|
||||
assert.match(view, /selectedDetail\?\.status === 'WAIT_CUSTOMER_CONFIRMATION'/)
|
||||
assert.match(view, /PDA 打托实测数据/)
|
||||
for (const label of ['托盘号', '库位', '箱数', '长 × 宽 × 高(cm)', '毛重(kg)', '体积(m³)', '完成时间']) assert.equal(view.includes(label), true)
|
||||
@@ -167,7 +211,7 @@ test('仓储转运详情自动加载 PDA 测量照片并释放 Blob URL', () =>
|
||||
})
|
||||
|
||||
test('PDA 完成后主单显示入库完成并保留客户确认测量入口', () => {
|
||||
assert.match(view, /row\.__status === 'WAIT_CUSTOMER_CONFIRMATION'.*确认测量数据/s)
|
||||
assert.match(view, /item\.__status === 'WAIT_CUSTOMER_CONFIRMATION'.*确认测量/s)
|
||||
assert.match(view, /selectedDetail\?\.status === 'WAIT_CUSTOMER_CONFIRMATION'.*确认测量数据/s)
|
||||
assert.doesNotMatch(view, /确认实测并入库/)
|
||||
})
|
||||
@@ -179,7 +223,7 @@ test('仓储转运可在新建页直接提交,也可保存草稿后编辑提
|
||||
assert.match(api, /warehouse-transfers\/\$\{encodeURIComponent\(transferNo\)\}\/submit/)
|
||||
assert.match(view, /submit\('draft'\).*保存草稿/s)
|
||||
assert.match(view, /async function openEdit\(transferNo: string\)/)
|
||||
assert.match(view, /row\.__status === 'DRAFT'[^>]*[\s\S]*?@click="openEdit\(row\['0'\]\)"[^>]*>编辑/)
|
||||
assert.match(view, /item\.__status === 'DRAFT'.*openEdit\(item\['0'\]\)/s)
|
||||
assert.match(view, /v-if="props\.service === 'transfer'"[^>]*type="primary"[^>]*native-type="submit"[^>]*>提交订单/)
|
||||
assert.match(view, /await updateTransfer\(editingTransferNo\.value, payload\)/)
|
||||
assert.match(view, /await submitTransfer\(transferNo\)/)
|
||||
|
||||
Reference in New Issue
Block a user