Merge remote-tracking branch 'origin/release/test' into release/test
This commit is contained in:
@@ -99,7 +99,7 @@ export interface LastMileOrderSummaryDto {
|
||||
deliveryOrderNo?: string | null
|
||||
outboundOrderNo?: string | null
|
||||
relabelOrderNo?: string | null
|
||||
relabelApplications?: Array<{ relabelNo: string; status: string; quantity: number; createdAt: string }>
|
||||
relabelApplications?: Array<{ relabelNo: string; status: string; quantity: number; quantityUnit?: string; createdAt: string }>
|
||||
relabelStatus?: string | null
|
||||
palletMergeFlag?: string | null
|
||||
/** Optional grouping facts returned by newer last-leg services. */
|
||||
@@ -359,7 +359,7 @@ export interface LastMileOrderDetailDto {
|
||||
order: { returnNo: string; status?: string; receivedQuantity: number; expectedQuantity: number }
|
||||
items?: Array<{ skuCode: string; productNameZh?: string; expectedQuantity: number; receivedQuantity: number }>
|
||||
receipts: Array<{ receiptNo: string; unitType: string; unitCode?: string; palletCode?: string; receivedQuantity: number; boxCount: number; signedAt: string }>
|
||||
cartons?: Array<{ receiptNo: string; palletCode?: string; cartonMark: string; countStatus: string; countedAt?: string; items: Array<{ skuCode: string; productNameZh?: string; quantity: number }> }>
|
||||
cartons?: Array<{ receiptNo: string; palletCode?: string; cartonMark: string; countStatus: string; countedAt?: string; items: Array<{ skuCode: string; productNameZh?: string; quantity: number }>; length?: number | null; width?: number | null; height?: number | null; weight?: number | null }>
|
||||
} | null
|
||||
order: LastMileOrderDto
|
||||
manualNodes?: ManualNodesResponse['nodes']
|
||||
|
||||
@@ -66,6 +66,8 @@ export interface RelabelSummaryDto {
|
||||
totalPackageCount?: number | null
|
||||
notifiedPackageCount?: number | null
|
||||
skuCodes?: string | null
|
||||
productNameZh?: string | null
|
||||
labelRequirement?: string | null
|
||||
totalFileId?: string | null
|
||||
totalFileName?: string | null
|
||||
totalFileUrl?: string | null
|
||||
@@ -87,6 +89,8 @@ export interface RelabelSummaryDto {
|
||||
|
||||
export interface RelabelItemDto {
|
||||
id: string
|
||||
operationType?: 'CARTON_LABEL' | 'SKU_LABEL' | 'CARTON_AND_SKU'
|
||||
skuItems?: Array<{ skuCode: string; newSkuCode: string; quantity: number; remark?: string }>
|
||||
skuCode?: string | null
|
||||
productNameZh?: string | null
|
||||
totalPackageCount?: number | null
|
||||
|
||||
@@ -57,6 +57,7 @@ function isSingleLineBusinessField(field: DetailField) { return field.label ===
|
||||
|
||||
const referenceValue = computed(() => lastMileOrderReference(detail.value?.order.orderNo, '订单编码待补充'))
|
||||
const isReturnOrder = computed(() => detail.value?.order.orderType === 'RETURN_RELABEL')
|
||||
const returnReceiptUnitLabel = computed(() => detail.value?.returnReceipt?.receipts.some(receipt => receipt.unitType === 'PARCEL') ? '包裹' : '箱')
|
||||
const showReturnForecast = computed(() => isReturnOrder.value && (detail.value?.returnReceipt?.order.status
|
||||
? detail.value.returnReceipt.order.status !== 'COMPLETED'
|
||||
: ['PENDING_REVIEW', 'PENDING_RECEIPT', 'PARTIALLY_RECEIVED', 'REJECTED', 'CANCELLED'].includes(detail.value?.order.currentStatus || '')))
|
||||
@@ -168,7 +169,7 @@ const cargoColumns = computed<DataTableColumn[]>(() => isReturnOrder.value ? [
|
||||
{ key: 'volume', prop: 'volume', label: '体积(m³)', width: 120, align: 'right' },
|
||||
])
|
||||
const documentColumns: DataTableColumn[] = [
|
||||
{ key: 'status', prop: 'status', label: '状态', minWidth: 130 }, { key: 'quantity', prop: 'quantity', label: '换标数量(件)', minWidth: 130 },
|
||||
{ key: 'status', prop: 'status', label: '状态', minWidth: 130 }, { key: 'quantity', prop: 'quantity', label: '换标数量(箱/件)', minWidth: 150, formatter: (row: { quantity?: number; quantityUnit?: string }) => `${row.quantity ?? '—'}${row.quantityUnit || '件'}` },
|
||||
{ key: 'type', prop: 'type', label: '单据类型', minWidth: 150 }, { key: 'number', prop: 'number', label: '单据编号', minWidth: 200 },
|
||||
{ key: 'createdAt', prop: 'createdAt', label: '生成时间', minWidth: 170 }, { key: 'quote', prop: 'quote', label: '报价', minWidth: 150 },
|
||||
]
|
||||
@@ -249,11 +250,12 @@ onMounted(load)
|
||||
<el-table-column prop="boxCount" label="实收箱数" width="100" />
|
||||
<el-table-column prop="receivedQuantity" label="实收数量" width="100" />
|
||||
</el-table>
|
||||
<h3>签收箱号明细({{ detail.returnReceipt.cartons?.length || 0 }} 箱)</h3>
|
||||
<h3>签收{{ returnReceiptUnitLabel }}明细({{ detail.returnReceipt.cartons?.length || 0 }} {{ returnReceiptUnitLabel }})</h3>
|
||||
<el-collapse v-if="detail.returnReceipt.cartons?.length">
|
||||
<el-collapse-item v-for="box in detail.returnReceipt.cartons" :key="box.cartonMark" :name="box.cartonMark">
|
||||
<template #title>{{ box.cartonMark }} · {{ box.countStatus === 'CONFIRMED' ? `已确认 · ${box.items.length} 种 SKU · ${box.items.reduce((sum, item) => sum + Number(item.quantity), 0)} 件` : box.countStatus === 'PENDING' ? '待清点' : '未留存箱内签收明细' }}</template>
|
||||
<p>签收单 {{ box.receiptNo }}<template v-if="box.palletCode"> · 托盘 {{ box.palletCode }}</template><template v-if="box.countedAt"> · 确认时间 {{ formatOverseasDateTime(box.countedAt) }}</template></p>
|
||||
<p v-if="box.length != null || box.width != null || box.height != null || box.weight != null">测量:{{ box.length ?? '—' }} × {{ box.width ?? '—' }} × {{ box.height ?? '—' }} cm · 重量 {{ box.weight ?? '—' }} kg</p>
|
||||
<el-table v-if="box.countStatus === 'CONFIRMED'" :data="box.items" size="small" border>
|
||||
<el-table-column prop="skuCode" label="SKU" min-width="220" />
|
||||
<el-table-column prop="productNameZh" label="中文品名" min-width="180" />
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { KeepAlive, defineComponent, h, ref } from 'vue'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { expect, it, vi } from 'vitest'
|
||||
import { getRelabelOrder } from '../api/relabelOrders'
|
||||
import RelabelOrderDetailView from './RelabelOrderDetailView.vue'
|
||||
|
||||
vi.mock('vue-router', () => ({ useRoute: () => ({ params: { orderId: 'RL-1' } }), useRouter: () => ({ push: vi.fn() }) }))
|
||||
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => false }) }))
|
||||
vi.mock('../api/relabelOrders', () => ({ getRelabelOrder: vi.fn(), confirmRelabelCompletion: vi.fn(), getRelabelReviewTask: vi.fn(), returnRelabelForRework: vi.fn(), downloadRelabelCompletionPhoto: vi.fn(), downloadRelabelPdf: vi.fn(), uploadRelabelPdf: vi.fn() }))
|
||||
|
||||
it('reloads authoritative status when a cached detail tab is reopened without double loading initially', async () => {
|
||||
vi.mocked(getRelabelOrder).mockResolvedValueOnce({ application: { id: '1', relabelNo: 'RL-1', status: 'PENDING_REVIEW' }, items: [], history: [] })
|
||||
.mockResolvedValueOnce({ application: { id: '1', relabelNo: 'RL-1', status: 'PENDING_PROCESSING' }, items: [], history: [] })
|
||||
const active = ref(true)
|
||||
const Host = defineComponent({ setup: () => () => h(KeepAlive, null, { default: () => active.value ? h(RelabelOrderDetailView) : h('div') }) })
|
||||
const wrapper = mount(Host, { global: { stubs: { OverseasReadOnlyDetail: true, AppDialog: true } } })
|
||||
await flushPromises()
|
||||
expect(getRelabelOrder).toHaveBeenCalledTimes(1)
|
||||
expect(wrapper.findComponent({ name: 'OverseasReadOnlyDetail' }).props('statusText')).toBe('待审核')
|
||||
active.value = false
|
||||
await flushPromises()
|
||||
active.value = true
|
||||
await flushPromises()
|
||||
expect(getRelabelOrder).toHaveBeenCalledTimes(2)
|
||||
expect(wrapper.findComponent({ name: 'OverseasReadOnlyDetail' }).props('statusText')).toBe('待处理')
|
||||
wrapper.unmount()
|
||||
})
|
||||
@@ -18,6 +18,9 @@ describe('RelabelOrderDetailView', () => {
|
||||
expect(source).toContain("PENDING_REVIEW: '待审核'")
|
||||
expect(source).toContain('value?.application.customerOrderNo || value?.application.lastlegOrderNo')
|
||||
expect(source).toContain('item.destinationFbaWarehouseCode || item.destinationWarehouseId')
|
||||
expect(source).toContain('items.flatMap(relabelSkuCodes)')
|
||||
expect(source).toContain('v-for="(sku, index) in item.skuItems"')
|
||||
expect(source).toContain('{{ sku.skuCode }} → {{ sku.newSkuCode }} × {{ sku.quantity }} 件')
|
||||
})
|
||||
|
||||
it('lets platform operators confirm completion or return PDA work for rework', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminDensity.css'
|
||||
import { computed, onMounted, ref, onBeforeUnmount } from 'vue'
|
||||
import { computed, onActivated, onMounted, ref, onBeforeUnmount } from 'vue'
|
||||
import { CheckCircle2, Eye, FileUp, Undo2 } from '@lucide/vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
@@ -14,6 +14,7 @@ import { downloadRelabelCompletionPhoto, downloadRelabelPdf, getRelabelOrder, up
|
||||
import { formatOverseasDateTime } from '../lastMilePresentation'
|
||||
import OverseasReadOnlyDetail from '../components/OverseasReadOnlyDetail.vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import { relabelSkuCodes } from '../utils/relabelSkuDisplay'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -80,7 +81,7 @@ const fieldValues = computed<Record<string, string | number>>(() => {
|
||||
资金状态: walletStatus(value?.application.walletStatus),
|
||||
创建时间: formatOverseasDateTime(value?.application.createdAt),
|
||||
更新时间: value?.history.length ? formatOverseasDateTime(value.history[value.history.length - 1]?.createdAt) : '—',
|
||||
商品SKU: items.map((item) => text(item.skuCode)).join('、') || '—',
|
||||
商品SKU: [...new Set(items.flatMap(relabelSkuCodes))].join('、') || '—',
|
||||
中文品名: items.map((item) => text(item.productNameZh)).join('、') || '—',
|
||||
箱数: Number(value?.application.totalPackageCount || 0),
|
||||
预算总价: '—',
|
||||
@@ -100,6 +101,7 @@ async function load() {
|
||||
}
|
||||
function back() { void router.push({ name: 'overseas-relabel-orders' }) }
|
||||
onMounted(load)
|
||||
onActivated(() => { if (!loading.value) void load() })
|
||||
onBeforeUnmount(() => { if (previewUrl.value) URL.revokeObjectURL(previewUrl.value) })
|
||||
async function uploadPdf(event: Event, scope: 'TOTAL' | 'ITEM', itemId?: string) {
|
||||
const input = event.target as HTMLInputElement; const file = input.files?.[0]; input.value = ''
|
||||
@@ -135,7 +137,7 @@ async function previewCompletionPhoto(fileId: string) {
|
||||
<table class="density-table">
|
||||
<thead><tr><th>我方箱唛</th><th>FBA 箱唛</th><th>商品 SKU</th><th>中文品名</th><th>FBA 货件编号</th><th>换标新 SKU</th><th>目的仓</th><th>明细PDF</th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="item in detail?.items || []" :key="item.id"><td class="mark-code">{{ text(item.cartonMark) }}</td><td class="mark-code">{{ text(item.fbaCartonMark) }}</td><td>{{ text(item.skuCode) }}</td><td>{{ text(item.productNameZh) }}</td><td>{{ text(item.shipmentNo) }}</td><td>{{ text(item.newSkuCode) }}</td><td>{{ text(item.destinationFbaWarehouseCode || item.destinationWarehouseId) }}</td><td><span v-if="item.relabelFileName" class="document-actions density-actions"><strong>{{ item.relabelFileName }}</strong><el-button class="density-action" data-tone="neutral" size="small" @click="previewPdf(item.relabelFileId, item.relabelFileName, 'ITEM', item.id)">预览</el-button></span><label v-else class="inline-file-picker"><FileUp :size="14" />上传<input type="file" accept="application/pdf,.pdf" :disabled="uploading" @change="uploadPdf($event, 'ITEM', item.id)" /></label></td></tr>
|
||||
<tr v-for="item in detail?.items || []" :key="item.id"><td class="mark-code">{{ text(item.cartonMark) }}</td><td class="mark-code">{{ text(item.fbaCartonMark) }}</td><td>{{ relabelSkuCodes(item).join('、') || '—' }}</td><td>{{ text(item.productNameZh) }}</td><td>{{ text(item.shipmentNo) }}</td><td><template v-if="item.skuItems?.length"><div v-for="(sku, index) in item.skuItems" :key="index">{{ sku.skuCode }} → {{ sku.newSkuCode }} × {{ sku.quantity }} 件</div></template><span v-else>{{ text(item.newSkuCode) }}</span></td><td>{{ text(item.destinationFbaWarehouseCode || item.destinationWarehouseId) }}</td><td><span v-if="item.relabelFileName" class="document-actions density-actions"><strong>{{ item.relabelFileName }}</strong><el-button class="density-action" data-tone="neutral" size="small" @click="previewPdf(item.relabelFileId, item.relabelFileName, 'ITEM', item.id)">预览</el-button></span><label v-else class="inline-file-picker"><FileUp :size="14" />上传<input type="file" accept="application/pdf,.pdf" :disabled="uploading" @change="uploadPdf($event, 'ITEM', item.id)" /></label></td></tr>
|
||||
<tr v-if="!detail?.items.length"><td class="empty-cell" colspan="8">暂无箱唛换标信息</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -42,6 +42,25 @@ enableAutoUnmount(afterEach)
|
||||
afterAll(() => vi.unstubAllGlobals())
|
||||
|
||||
describe('RelabelOrdersView', () => {
|
||||
it('shows every SKU and quantity in the review dialog while keeping one box', async () => {
|
||||
vi.mocked(listRelabelOrders).mockResolvedValueOnce({ records: [{ id: '1', relabelNo: 'RL-MULTI', skuCodes: 'SKU-A,SKU-B', totalPackageCount: 1, status: 'PENDING_REVIEW' }], page: 1, size: 10, total: 1, pages: 1 })
|
||||
vi.mocked(getRelabelOrder).mockResolvedValueOnce({ application: { id: '1', relabelNo: 'RL-MULTI', totalPackageCount: 1 }, items: [{ id: '1', skuCode: 'SKU-A', newSkuCode: 'NEW-A', skuItems: [{ skuCode: 'SKU-A', newSkuCode: 'NEW-A', quantity: 10 }, { skuCode: 'SKU-B', newSkuCode: 'NEW-B', quantity: 5 }] }], history: [] })
|
||||
const wrapper = mount(RelabelOrdersView, { attachTo: document.body, global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('SKU-A,SKU-B')
|
||||
await wrapper.findAll('button').find(button => button.text().trim() === '审核')!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(formItem(wrapper, '商品SKU').findComponent({ name: 'ElInput' }).props('modelValue')).toBe('SKU-A、SKU-B')
|
||||
expect(formItem(wrapper, '新SKU').findComponent({ name: 'ElInput' }).props('modelValue')).toBe('NEW-A × 10 件;NEW-B × 5 件')
|
||||
expect(formItem(wrapper, '箱数').findComponent({ name: 'ElInputNumber' }).props('modelValue')).toBe(1)
|
||||
})
|
||||
it('renders product names and labeling requirements supplied by the list projection', async () => {
|
||||
vi.mocked(listRelabelOrders).mockResolvedValueOnce({ records: [{ id: '1', relabelNo: 'RL-1', productNameZh: '商品甲、商品乙', labelRequirement: '贴标要求111', status: 'PENDING_PROCESSING' }], page: 1, size: 10, total: 1, pages: 1 })
|
||||
const wrapper = mount(RelabelOrdersView, { attachTo: document.body, global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('商品甲、商品乙')
|
||||
expect(wrapper.text()).toContain('贴标要求111')
|
||||
})
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
|
||||
vi.stubGlobal('localStorage', memoryStorage())
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminDensity.css'
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { computed, onActivated, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { relabelSkuCodes } from '../utils/relabelSkuDisplay'
|
||||
import { Plus, PackagePlus } from '@lucide/vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
@@ -94,16 +95,15 @@ async function openForm(mode: 'create' | 'edit', source?: object) {
|
||||
detailLoading.value = true
|
||||
try {
|
||||
const detail = await getRelabelOrder(String(row.orderNo))
|
||||
const item = detail.items?.[0]
|
||||
Object.assign(form, {
|
||||
orderNo: String(detail.application?.relabelNo || row.orderNo || ''),
|
||||
relatedOrderNo: String(detail.application?.lastlegOrderNo || detail.application?.customerOrderNo || row.relatedOrderNo || ''),
|
||||
sku: String(item?.skuCode || row.sku || ''),
|
||||
productName: String(item?.productNameZh || row.productName || ''),
|
||||
cartons: optionalNumber(item?.totalPackageCount ?? detail.application?.totalPackageCount ?? row.cartons),
|
||||
labelRequirement: String(item?.labelRequirement || row.labelRequirement || ''),
|
||||
shipmentNo: String(item?.shipmentNo || ''),
|
||||
newSkuCode: String(item?.newSkuCode || ''),
|
||||
sku: [...new Set(detail.items.flatMap(relabelSkuCodes))].join('、') || row.sku || '',
|
||||
productName: [...new Set(detail.items.map(line => line.productNameZh?.trim()).filter(Boolean))].join('、') || row.productName || '',
|
||||
cartons: optionalNumber(detail.application?.totalPackageCount ?? row.cartons),
|
||||
labelRequirement: [...new Set(detail.items.map(line => line.labelRequirement?.trim()).filter(Boolean))].join(';') || row.labelRequirement || '',
|
||||
shipmentNo: [...new Set(detail.items.map(line => line.shipmentNo?.trim()).filter(Boolean))].join('、'),
|
||||
newSkuCode: detail.items.flatMap(line => line.skuItems?.length ? line.skuItems.map(sku => `${sku.newSkuCode} × ${sku.quantity} 件`) : [line.newSkuCode || '']).filter(Boolean).join(';'),
|
||||
relabelCost: optionalNumber(detail.application?.chargeAmount ?? row.relabelCost),
|
||||
remark: String(detail.remark || detail.application?.reviewRemark || row.remark || ''),
|
||||
})
|
||||
@@ -205,7 +205,7 @@ async function load(model: QueryModel = filters.value) {
|
||||
const result = await listRelabelOrders({ page: page.value, size: pageSize.value, relabelNo: String(filters.value.relabelNo || '').trim() || undefined, skuCode: String(filters.value.sku || '').trim() || undefined, status: String(filters.value.status || '').trim() || undefined })
|
||||
rows.value = result.records.map((item) => ({
|
||||
customerId: '', orderNo: String(item.relabelNo), relatedOrderNo: String(item.lastlegOrderNo || item.customerOrderNo || '—'), sku: String(item.skuCodes || '—'),
|
||||
productName: '—', cartons: Number(item.totalPackageCount || 0), budgetTotal: null, relabelCost: Number(item.chargeAmount || 0), walletStatus: walletStatus(item.walletStatus), walletErrorMessage: item.walletErrorMessage || '', labelRequirement: '—', fileName: '—', status: relabelStatus(item.status), rawStatus: String(item.status || ''), remark: '',
|
||||
productName: item.productNameZh?.trim() || '—', cartons: Number(item.totalPackageCount || 0), budgetTotal: null, relabelCost: Number(item.chargeAmount || 0), walletStatus: walletStatus(item.walletStatus), walletErrorMessage: item.walletErrorMessage || '', labelRequirement: item.labelRequirement?.trim() || '—', fileName: '—', status: relabelStatus(item.status), rawStatus: String(item.status || ''), remark: '',
|
||||
reviewChannel: item.reviewChannel || undefined,
|
||||
createdAt: formatOverseasDateTime(item.createdAt),
|
||||
}))
|
||||
@@ -216,6 +216,7 @@ async function load(model: QueryModel = filters.value) {
|
||||
function search(model: QueryModel) { page.value = 1; void load(model) }
|
||||
function reset(model: QueryModel) { page.value = 1; void load(model) }
|
||||
onMounted(load)
|
||||
onActivated(() => { if (!loading.value) void load() })
|
||||
watch(dialogOpen, (open) => { if (!open) createIdempotencyKey.value = '' })
|
||||
</script>
|
||||
|
||||
@@ -241,7 +242,7 @@ watch(dialogOpen, (open) => { if (!open) createIdempotencyKey.value = '' })
|
||||
<el-col :span="12"><el-form-item label="关联订单" required><el-input v-model="form.relatedOrderNo" :disabled="dialogMode === 'edit'" placeholder="请输入关联订单号" /></el-form-item></el-col>
|
||||
<el-col v-if="dialogMode === 'create'" :span="12"><el-form-item label="客户" required><el-select v-model="form.customerId" filterable :loading="customersLoading" placeholder="请选择 ACTIVE 客户"><el-option v-for="item in customers" :key="String(item.id)" :label="item.tenantName" :value="String(item.id)" /></el-select></el-form-item></el-col>
|
||||
<el-col v-if="dialogMode === 'create'" :span="12"><el-form-item label="公共海外收货仓" required><el-select v-model="form.warehouseId" filterable :loading="warehousesLoading" placeholder="请选择 ACTIVE+RECEIVING 海外仓"><el-option v-for="item in warehouses" :key="item.id" :label="`${item.warehouseName} · ${item.warehouseCode}`" :value="String(item.id)" /></el-select></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="商品SKU" required><el-input v-model="form.sku" :disabled="dialogMode === 'edit'" placeholder="请输入商品SKU" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="商品SKU" required><el-input v-model="form.sku" :type="dialogMode === 'edit' ? 'textarea' : 'text'" :autosize="{ minRows: 1, maxRows: 4 }" :disabled="dialogMode === 'edit'" placeholder="请输入商品SKU" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="中文品名" required><el-input v-model="form.productName" :disabled="dialogMode === 'edit'" placeholder="请输入中文品名" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="箱数" required><el-input-number v-model="form.cartons" :disabled="dialogMode === 'edit'" :min="1" :precision="0" controls-position="right" placeholder="请输入箱数" /></el-form-item></el-col>
|
||||
<el-col v-if="dialogMode === 'create'" :span="12"><el-form-item label="截止时间" required><el-date-picker v-model="form.labelDeadline" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择截止时间" /></el-form-item></el-col>
|
||||
@@ -251,7 +252,7 @@ watch(dialogOpen, (open) => { if (!open) createIdempotencyKey.value = '' })
|
||||
</el-row><h3>作业要求</h3><el-row :gutter="16">
|
||||
<el-col :span="12"><el-form-item label="贴标要求" required><el-select v-model="form.labelRequirement" :disabled="dialogMode === 'edit'" placeholder="请选择贴标要求"><el-option v-for="value in ['换箱唛', '换SKU标', '换箱唛和SKU标']" :key="value" :label="value" :value="value" /></el-select></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="货件号" required><el-input v-model="form.shipmentNo" :disabled="dialogMode === 'edit'" placeholder="请输入货件号" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="新SKU" required><el-input v-model="form.newSkuCode" :disabled="dialogMode === 'edit'" placeholder="请输入新SKU" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="新SKU" required><el-input v-model="form.newSkuCode" :type="dialogMode === 'edit' ? 'textarea' : 'text'" :autosize="{ minRows: 1, maxRows: 4 }" :disabled="dialogMode === 'edit'" placeholder="请输入新SKU" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="换标费用(CNY)" required><el-input-number v-model="form.relabelCost" :disabled="dialogMode === 'edit'" :min="0.01" :precision="2" controls-position="right" placeholder="请输入换标费用" /></el-form-item></el-col>
|
||||
<el-col :span="24"><el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="请输入备注(选填)" /></el-form-item></el-col>
|
||||
</el-row></el-form>
|
||||
|
||||
@@ -668,11 +668,10 @@ const tableColumns = computed(() =>
|
||||
warehouseName(row.overseasWarehouseId)
|
||||
}}</template>
|
||||
<template #movement-4="{ row }">{{
|
||||
row.movementType === "RECEIPT"
|
||||
? "退货签收"
|
||||
: row.movementType === "MERGE"
|
||||
? "退货合箱"
|
||||
: "退货出库"
|
||||
({ RECEIPT: "退货签收", MERGE: "退货合箱", OUTBOUND: "退货出库",
|
||||
SKU_CONVERSION_OUT: "SKU换标", SKU_CONVERSION_IN: "SKU换标",
|
||||
SKU_CONVERSION_REVERSE_IN: "SKU换标撤销", SKU_CONVERSION_REVERSE_OUT: "SKU换标撤销"
|
||||
} as Record<string, string>)[row.movementType] || row.movementType || "—"
|
||||
}}</template>
|
||||
<template #plan-customer="{ row }">{{ customerName(row.customerId) }}</template>
|
||||
<template #plan-return-nos="{ row }">{{
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { expect, it } from 'vitest'
|
||||
import { relabelSkuCodes } from './relabelSkuDisplay'
|
||||
|
||||
it('uses every nested SKU, deduplicates and preserves carton-only SKU lists', () => {
|
||||
expect(relabelSkuCodes({ skuCode: 'FIRST', skuItems: [{ skuCode: 'FIRST' }, { skuCode: 'SECOND' }, { skuCode: 'FIRST' }] })).toEqual(['FIRST', 'SECOND'])
|
||||
expect(relabelSkuCodes({ skuCode: 'FIRST,SECOND', skuItems: [] })).toEqual(['FIRST', 'SECOND'])
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
interface RelabelSkuSource {
|
||||
skuCode?: string | null
|
||||
skuItems?: Array<{ skuCode: string }> | null
|
||||
}
|
||||
|
||||
export function relabelSkuCodes(item: RelabelSkuSource): string[] {
|
||||
const codes = item.skuItems?.length ? item.skuItems.map(line => line.skuCode) : (item.skuCode || '').split(',')
|
||||
return [...new Set(codes.map(code => code.trim()).filter(Boolean))]
|
||||
}
|
||||
Reference in New Issue
Block a user