This commit is contained in:
2026-10-16 11:42:59 +08:00
parent 4d7b6eb6dc
commit 9b58c12012
8 changed files with 180 additions and 46 deletions
@@ -45,7 +45,7 @@ export interface TransferCreate {
inboundCode?: string; sourceLastlegOrderIds?: string[]; operatingWarehouseId: string; fbaWarehouseCode?: string; destinationType?: 'NONE' | 'FBA' | 'PRIVATE' inboundCode?: string; sourceLastlegOrderIds?: string[]; operatingWarehouseId: string; fbaWarehouseCode?: string; destinationType?: 'NONE' | 'FBA' | 'PRIVATE'
destinationWarehouseId?: string; destinationWarehouseCode?: string; destinationWarehouseName?: string destinationWarehouseId?: string; destinationWarehouseCode?: string; destinationWarehouseName?: string
privateDestinationAddress?: string; privateDestinationPostalCode?: string; privateDestinationContactName?: string; privateDestinationContactPhone?: string privateDestinationAddress?: string; privateDestinationPostalCode?: string; privateDestinationContactName?: string; privateDestinationContactPhone?: string
deliveryAddress: string; packageCount: number; volumeCbm: number; deliveryMethod: string; estimatedArrivalAt: string; estimatedShipAt?: string deliveryAddress?: string; packageCount: number; volumeCbm: number; deliveryMethod?: string; estimatedArrivalAt?: string; estimatedShipAt?: string
fbaShipmentNo?: string; trackingNo?: string; fbaBoxNos?: string; remark?: string; needRelabel: boolean; idempotencyKey: string fbaShipmentNo?: string; trackingNo?: string; fbaBoxNos?: string; remark?: string; needRelabel: boolean; idempotencyKey: string
labelDeadline?: string; pickupContactName?: string; pickupContactPhone?: string; relabelContactName?: string; relabelContactPhone?: string; relabelChargeAmount?: number; relabelDeliveryMethod?: RelabelDeliveryMethod labelDeadline?: string; pickupContactName?: string; pickupContactPhone?: string; relabelContactName?: string; relabelContactPhone?: string; relabelChargeAmount?: number; relabelDeliveryMethod?: RelabelDeliveryMethod
items: Array<{ skuCode?: string; productNameZh?: string; quantity: number; packageCount: number; shippingMark?: string }> items: Array<{ skuCode?: string; productNameZh?: string; quantity: number; packageCount: number; shippingMark?: string }>
@@ -142,3 +142,8 @@ export interface DiscardPhoto { fileId: string; palletNo: string; fileName: stri
export function downloadDiscardPhoto(discardNo: string, fileId: string) { export function downloadDiscardPhoto(discardNo: string, fileId: string) {
return apiBlobRequest({ method: 'GET', url: `/merchant/v1/last-leg/cargo-discards/${encodeURIComponent(discardNo)}/photos/files/${encodeURIComponent(fileId)}/download` }) return apiBlobRequest({ method: 'GET', url: `/merchant/v1/last-leg/cargo-discards/${encodeURIComponent(discardNo)}/photos/files/${encodeURIComponent(fileId)}/download` })
} }
export interface TransferSourceCarton { cartonMark: string; productNameZh?: string; quantity?: number; destinationWarehouseCode?: string; destinationWarehouseName?: string; items: Array<{skuCode?: string; quantity?: number}> }
export function getTransferSourceCartons(operatingWarehouseId: string, inboundCode: string) {
return apiRequest<TransferSourceCarton[]>({method:'GET',url:'/merchant/v1/last-leg/warehouse-transfers/inventory-source-cartons',params:{operatingWarehouseId,inboundCode}})
}
@@ -14,6 +14,9 @@ function harness(kind = 'disposal', overrides = {}) {
const api = { const api = {
getDiscard: async () => ({ summary: { status: 'PDA_COMPLETED' }, photos: [{ fileId: '101' }] }), getDiscard: async () => ({ summary: { status: 'PDA_COMPLETED' }, photos: [{ fileId: '101' }] }),
getRelabel: async () => ({ application: { status: 'CUSTOMER_CONFIRM_PENDING' }, completionEvidence: { photoFileIds: ['101'] } }), getRelabel: async () => ({ application: { status: 'CUSTOMER_CONFIRM_PENDING' }, completionEvidence: { photoFileIds: ['101'] } }),
getTransfer: async () => ({ status: 'WAIT_CUSTOMER_CONFIRMATION', measurementPhotos: [{ fileId: '101' }], cartons: [{ cartonMark: 'BOX-1', status: 'COUNTED', weight: 5 }], pallets: [{ palletNo: 'PLT-1', grossWeightKg: 5 }] }),
downloadTransferMeasurementPhoto: async () => ({}),
confirmTransferMeasurement: async no => calls.push(['transfer', no]),
downloadDiscardPhoto: async () => ({}), downloadRelabelCompletionPhoto: async () => ({}), downloadDiscardPhoto: async () => ({}), downloadRelabelCompletionPhoto: async () => ({}),
confirmDiscardCompletion: async no => calls.push(['discard', no]), confirmDiscardCompletion: async no => calls.push(['discard', no]),
confirmRelabelCompletion: async no => calls.push(['relabel', no]), ...overrides, confirmRelabelCompletion: async no => calls.push(['relabel', no]), ...overrides,
@@ -24,11 +27,11 @@ function harness(kind = 'disposal', overrides = {}) {
useAuthStore: () => ({ hasPermission: () => true }), ElMessage: { success() {}, error() {} }, useAuthStore: () => ({ hasPermission: () => true }), ElMessage: { success() {}, error() {} },
URL: { createObjectURL: () => 'blob:photo', revokeObjectURL: url => revoked.push(url) }, ...api, URL: { createObjectURL: () => 'blob:photo', revokeObjectURL: url => revoked.push(url) }, ...api,
} }
const state = new Function(...Object.keys(env), js + '\nreturn { loadEvidence, loadPhoto, submit, clear, checked, canConfirm, urls, failed, error, busy };')(...Object.values(env)) const state = new Function(...Object.keys(env), js + '\nreturn { loadEvidence, loadPhoto, submit, clear, checked, canConfirm, urls, failed, error, busy, measurement };')(...Object.values(env))
return { ...state, props, calls, events, revoked } return { ...state, props, calls, events, revoked }
} }
for (const kind of ['disposal', 'labeling']) { for (const kind of ['disposal', 'labeling', 'transfer']) {
test(`${kind}: opening loads photos without confirming; only reviewed evidence can be submitted`, async () => { test(`${kind}: opening loads photos without confirming; only reviewed evidence can be submitted`, async () => {
const state = harness(kind) const state = harness(kind)
await state.loadEvidence() await state.loadEvidence()
@@ -37,12 +40,33 @@ for (const kind of ['disposal', 'labeling']) {
state.checked.value = true state.checked.value = true
assert.equal(state.canConfirm.value, true) assert.equal(state.canConfirm.value, true)
await state.submit() await state.submit()
assert.deepEqual(state.calls, [[kind === 'disposal' ? 'discard' : 'relabel', 'CD/1']]) assert.deepEqual(state.calls, [[kind === 'transfer' ? 'transfer' : kind === 'disposal' ? 'discard' : 'relabel', 'CD/1']])
assert.deepEqual(state.events, [['confirmed', 'CD/1'], ['update:modelValue', false]]) assert.deepEqual(state.events, [['confirmed', 'CD/1'], ['update:modelValue', false]])
state.clear(); assert.deepEqual(state.revoked, ['blob:photo']) state.clear(); assert.deepEqual(state.revoked, ['blob:photo'])
}) })
} }
test('transfer confirmation reads current measurements and blocks missing, failed or outdated evidence', async () => {
const state = harness('transfer', { downloadTransferMeasurementPhoto: async () => { throw Error('offline') } })
await state.loadEvidence()
assert.equal(state.measurement.value.pallets[0].grossWeightKg, 5)
assert.equal(state.measurement.value.cartons[0].cartonMark, 'BOX-1')
state.checked.value = true
await state.submit()
assert.equal(state.canConfirm.value, false)
assert.deepEqual(state.calls, [])
for (const detail of [
{ status: 'WAIT_CUSTOMER_CONFIRMATION', measurementPhotos: [] },
{ status: 'COMPLETED', measurementPhotos: [{ fileId: '101' }] },
{ status: 'WAIT_CUSTOMER_CONFIRMATION', customerConfirmedAt: '2026-10-16', measurementPhotos: [{ fileId: '101' }] },
]) {
const invalid = harness('transfer', { getTransfer: async () => detail })
await invalid.loadEvidence(); invalid.checked.value = true; await invalid.submit()
assert.equal(invalid.canConfirm.value, false)
assert.deepEqual(invalid.calls, [])
}
})
test('photo failure blocks confirmation and supports retry', async () => { test('photo failure blocks confirmation and supports retry', async () => {
let reject = true let reject = true
const state = harness('disposal', { downloadDiscardPhoto: async () => { if (reject) throw Error('offline'); return {} } }) const state = harness('disposal', { downloadDiscardPhoto: async () => { if (reject) throw Error('offline'); return {} } })
@@ -2,12 +2,20 @@
import { computed, onBeforeUnmount, ref, watch } from 'vue' import { computed, onBeforeUnmount, ref, watch } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { useAuthStore } from '@/modules/auth/public' import { useAuthStore } from '@/modules/auth/public'
import { confirmDiscardCompletion, confirmRelabelCompletion, downloadDiscardPhoto, downloadRelabelCompletionPhoto, getDiscard, getRelabel, type DiscardPhoto } from '../api/warehouseServices' import { confirmDiscardCompletion, confirmRelabelCompletion, confirmTransferMeasurement, downloadDiscardPhoto, downloadRelabelCompletionPhoto, downloadTransferMeasurementPhoto, getDiscard, getRelabel, getTransfer, type DiscardPhoto } from '../api/warehouseServices'
const props = defineProps<{ modelValue: boolean; kind: 'disposal' | 'labeling'; documentNo: string }>() const props = defineProps<{ modelValue: boolean; kind: 'disposal' | 'labeling' | 'transfer'; documentNo: string }>()
const emit = defineEmits<{ 'update:modelValue': [value: boolean]; confirmed: [documentNo: string] }>() const emit = defineEmits<{ 'update:modelValue': [value: boolean]; confirmed: [documentNo: string] }>()
const auth = useAuthStore() const auth = useAuthStore()
const title = computed(() => props.kind === 'disposal' ? '废弃确认' : '贴标完成确认') const title = computed(() => props.kind === 'transfer' ? '确认测量数据' : props.kind === 'disposal' ? '废弃确认' : '贴标完成确认')
interface MeasurementEvidence {
status?: string
customerConfirmedAt?: string
cartons?: Array<{ cartonMark: string; status: string; length?: number; width?: number; height?: number; weight?: number }>
pallets?: Array<{ palletNo?: string; sequenceNo?: number; cartonCount?: number; lengthCm?: number; widthCm?: number; heightCm?: number; grossWeightKg?: number; volumeCbm?: number }>
measurementPhotos?: Array<{ fileId: string | number }>
}
const measurement = ref<MeasurementEvidence | null>(null)
const loading = ref(false) const loading = ref(false)
const busy = ref(false) const busy = ref(false)
const error = ref('') const error = ref('')
@@ -21,18 +29,19 @@ let generation = 0
const previewUrls = computed(() => fileIds.value.map(id => urls.value[id]).filter(Boolean)) const previewUrls = computed(() => fileIds.value.map(id => urls.value[id]).filter(Boolean))
const canConfirm = computed(() => eligible.value && checked.value && !loading.value && !busy.value && !error.value const canConfirm = computed(() => eligible.value && checked.value && !loading.value && !busy.value && !error.value
&& fileIds.value.length > 0 && fileIds.value.every(id => Boolean(urls.value[id])) && fileIds.value.length > 0 && fileIds.value.every(id => Boolean(urls.value[id]))
&& auth.hasPermission(`merchant:overseas-warehouse:${props.kind === 'disposal' ? 'discard' : 'relabel'}:confirm`)) && auth.hasPermission(`merchant:overseas-warehouse:${props.kind === 'transfer' ? 'transfer' : props.kind === 'disposal' ? 'discard' : 'relabel'}:confirm`))
function clear() { function clear() {
generation++ generation++
Object.values(urls.value).forEach(URL.revokeObjectURL) Object.values(urls.value).forEach(URL.revokeObjectURL)
urls.value = {}; failed.value = {}; photoLoading.value = {}; fileIds.value = []; checked.value = false; eligible.value = false urls.value = {}; failed.value = {}; photoLoading.value = {}; fileIds.value = []; checked.value = false; eligible.value = false
measurement.value = null
} }
async function loadPhoto(fileId: string) { async function loadPhoto(fileId: string) {
if (photoLoading.value[fileId]) return if (photoLoading.value[fileId]) return
const currentGeneration = generation; const no = props.documentNo; const kind = props.kind const currentGeneration = generation; const no = props.documentNo; const kind = props.kind
photoLoading.value[fileId] = true; failed.value[fileId] = false photoLoading.value[fileId] = true; failed.value[fileId] = false
try { try {
const blob = kind === 'disposal' ? await downloadDiscardPhoto(no, fileId) : await downloadRelabelCompletionPhoto(no, fileId) const blob = kind === 'transfer' ? await downloadTransferMeasurementPhoto(no, fileId) : kind === 'disposal' ? await downloadDiscardPhoto(no, fileId) : await downloadRelabelCompletionPhoto(no, fileId)
if (generation !== currentGeneration || !props.modelValue) return if (generation !== currentGeneration || !props.modelValue) return
if (urls.value[fileId]) URL.revokeObjectURL(urls.value[fileId]) if (urls.value[fileId]) URL.revokeObjectURL(urls.value[fileId])
urls.value[fileId] = URL.createObjectURL(blob) urls.value[fileId] = URL.createObjectURL(blob)
@@ -43,7 +52,13 @@ async function loadEvidence() {
clear(); error.value = ''; loading.value = true clear(); error.value = ''; loading.value = true
const currentGeneration = generation const currentGeneration = generation
try { try {
if (props.kind === 'disposal') { if (props.kind === 'transfer') {
const detail = await getTransfer(props.documentNo) as MeasurementEvidence
if (generation !== currentGeneration) return
measurement.value = detail
eligible.value = detail.status === 'WAIT_CUSTOMER_CONFIRMATION' && !detail.customerConfirmedAt
fileIds.value = (detail.measurementPhotos || []).map(photo => String(photo.fileId))
} else if (props.kind === 'disposal') {
const detail = await getDiscard(props.documentNo) as { summary?: { status?: string }; photos?: DiscardPhoto[] } const detail = await getDiscard(props.documentNo) as { summary?: { status?: string }; photos?: DiscardPhoto[] }
if (generation !== currentGeneration) return if (generation !== currentGeneration) return
eligible.value = detail.summary?.status === 'PDA_COMPLETED' eligible.value = detail.summary?.status === 'PDA_COMPLETED'
@@ -56,7 +71,7 @@ async function loadEvidence() {
} }
fileIds.value = [...new Set(fileIds.value)] fileIds.value = [...new Set(fileIds.value)]
if (!eligible.value) error.value = '当前单据已确认或尚未完成 PDA 作业,请刷新列表核对。' if (!eligible.value) error.value = '当前单据已确认或尚未完成 PDA 作业,请刷新列表核对。'
else if (!fileIds.value.length) error.value = '暂无 PDA 完工作业照片,暂不能确认。' else if (!fileIds.value.length) error.value = props.kind === 'transfer' ? '暂无 PDA 测量打托照片,暂不能确认。' : '暂无 PDA 完工作业照片,暂不能确认。'
await Promise.all(fileIds.value.map(loadPhoto)) await Promise.all(fileIds.value.map(loadPhoto))
} catch (e) { if (generation === currentGeneration) error.value = e instanceof Error ? e.message : '作业照片加载失败' } } catch (e) { if (generation === currentGeneration) error.value = e instanceof Error ? e.message : '作业照片加载失败' }
finally { if (generation === currentGeneration) loading.value = false } finally { if (generation === currentGeneration) loading.value = false }
@@ -65,9 +80,10 @@ async function submit() {
if (!canConfirm.value) return if (!canConfirm.value) return
busy.value = true busy.value = true
try { try {
if (props.kind === 'disposal') await confirmDiscardCompletion(props.documentNo) if (props.kind === 'transfer') await confirmTransferMeasurement(props.documentNo)
else if (props.kind === 'disposal') await confirmDiscardCompletion(props.documentNo)
else await confirmRelabelCompletion(props.documentNo) else await confirmRelabelCompletion(props.documentNo)
ElMessage.success(props.kind === 'disposal' ? '废弃已确认,单据已完结' : '贴标已确认,订单进入后续出库流程') ElMessage.success(props.kind === 'transfer' ? '仓库实测数据已确认' : props.kind === 'disposal' ? '废弃已确认,单据已完结' : '贴标已确认,订单进入后续出库流程')
emit('confirmed', props.documentNo) emit('confirmed', props.documentNo)
emit('update:modelValue', false) emit('update:modelValue', false)
} catch (e) { ElMessage.error(e instanceof Error ? e.message : '确认失败,请重试') } } catch (e) { ElMessage.error(e instanceof Error ? e.message : '确认失败,请重试') }
@@ -80,7 +96,22 @@ onBeforeUnmount(clear)
<template> <template>
<el-dialog :model-value="modelValue" :title="title" width="760px" :close-on-click-modal="false" :close-on-press-escape="!busy" :show-close="!busy" @update:model-value="!busy && emit('update:modelValue', $event)"> <el-dialog :model-value="modelValue" :title="title" width="760px" :close-on-click-modal="false" :close-on-press-escape="!busy" :show-close="!busy" @update:model-value="!busy && emit('update:modelValue', $event)">
<p>单号:{{ documentNo }}</p> <p>单号:{{ documentNo }}</p>
<p>请核对以下 PDA 作业照片,确认作业结果无误后再提交。</p> <p>{{ kind === 'transfer' ? '请核对 PDA 回传的箱数、尺寸、重量、打托数据及现场照片,确认无误后再提交。' : '请核对以下 PDA 作业照片,确认作业结果无误后再提交。' }}</p>
<template v-if="kind === 'transfer' && measurement">
<p>实际签收箱数:{{ measurement.cartons?.filter(carton => carton.status === 'COUNTED').length ?? 0 }} 箱;托盘数:{{ measurement.pallets?.length ?? 0 }} 托</p>
<el-table :data="measurement.pallets || []" max-height="220" size="small" border empty-text="暂无 PDA 打托测量数据">
<el-table-column label="托盘号" min-width="180"><template #default="{ row }">{{ row.palletNo || `第${row.sequenceNo || '—'}托` }}</template></el-table-column>
<el-table-column prop="cartonCount" label="箱数" width="65" />
<el-table-column label="尺寸(cm)" min-width="130"><template #default="{ row }">{{ row.lengthCm ?? '—' }} × {{ row.widthCm ?? '—' }} × {{ row.heightCm ?? '—' }}</template></el-table-column>
<el-table-column prop="grossWeightKg" label="毛重(kg)" width="95" />
<el-table-column prop="volumeCbm" label="体积(m³)" width="95" />
</el-table>
<el-table :data="measurement.cartons?.filter(carton => carton.status === 'COUNTED') || []" max-height="220" size="small" border empty-text="暂无已签收箱子">
<el-table-column prop="cartonMark" label="箱唛" min-width="200" />
<el-table-column label="尺寸(cm)" min-width="180"><template #default="{ row }">{{ row.length ?? '—' }} × {{ row.width ?? '—' }} × {{ row.height ?? '—' }}</template></el-table-column>
<el-table-column prop="weight" label="重量(kg)" width="110" />
</el-table>
</template>
<p v-if="loading">正在读取作业照片…</p> <p v-if="loading">正在读取作业照片…</p>
<el-alert v-if="error" :title="error" type="error" :closable="false" /> <el-alert v-if="error" :title="error" type="error" :closable="false" />
<el-button v-if="error" :disabled="loading || busy" @click="loadEvidence">重新读取</el-button> <el-button v-if="error" :disabled="loading || busy" @click="loadEvidence">重新读取</el-button>
@@ -91,10 +122,10 @@ onBeforeUnmount(clear)
<el-button v-else :disabled="busy" @click="loadPhoto(fileId)">{{ failed[fileId] ? '照片加载失败,点击重试' : '加载照片' }}</el-button> <el-button v-else :disabled="busy" @click="loadPhoto(fileId)">{{ failed[fileId] ? '照片加载失败,点击重试' : '加载照片' }}</el-button>
</div> </div>
</div> </div>
<el-checkbox v-model="checked" :disabled="loading || busy || !!error || previewUrls.length !== fileIds.length">我已核对 PDA 作业照片,确认作业结果无误</el-checkbox> <el-checkbox v-model="checked" :disabled="loading || busy || !!error || previewUrls.length !== fileIds.length">{{ kind === 'transfer' ? '我已核对 PDA 实测数据及现场照片,确认无误' : '我已核对 PDA 作业照片,确认作业结果无误' }}</el-checkbox>
<template #footer> <template #footer>
<el-button :disabled="busy" @click="emit('update:modelValue', false)">取消</el-button> <el-button :disabled="busy" @click="emit('update:modelValue', false)">取消</el-button>
<el-button type="primary" :loading="busy" :disabled="!canConfirm" @click="submit">{{ kind === 'disposal' ? '确认废弃完结' : '确认贴标完成' }}</el-button> <el-button type="primary" :loading="busy" :disabled="!canConfirm" @click="submit">{{ kind === 'transfer' ? '确认数据' : kind === 'disposal' ? '确认废弃完结' : '确认贴标完成' }}</el-button>
</template> </template>
</el-dialog> </el-dialog>
</template> </template>
@@ -39,6 +39,8 @@ test('箱子在签收箱号明细展示一次,真实托盘和缺少箱号明
test('parcel waybills appear as tracking numbers while generated parcel codes remain physical codes', () => { test('parcel waybills appear as tracking numbers while generated parcel codes remain physical codes', () => {
assert.equal(exports.receiptTrackingNo({ unitType: 'PARCEL', unitCode: 'sf561626' }), 'sf561626') assert.equal(exports.receiptTrackingNo({ unitType: 'PARCEL', unitCode: 'sf561626' }), 'sf561626')
assert.equal(exports.receiptTrackingNo({ unitType: 'PARCEL', unitCode: 'PK0123456789ABCDEF' }), '') assert.equal(exports.receiptTrackingNo({ unitType: 'PARCEL', unitCode: 'PK0123456789ABCDEF' }), '')
assert.equal(exports.receiptTrackingNo({ unitType: 'PARCEL', unitCode: 'PK-FUW3Q9N0B8RL' }), '')
assert.equal(exports.receiptTrackingNo({ unitType: 'PARCEL', unitCode: 'PK-FUW3Q9N0B8RL', trackingNo: ' SF123 ' }), 'SF123')
assert.equal(exports.receiptTrackingNo({ unitType: 'PARCEL', unitCode: 'PK0123456789ABCDEF', trackingNo: ' SF123 ' }), 'SF123') assert.equal(exports.receiptTrackingNo({ unitType: 'PARCEL', unitCode: 'PK0123456789ABCDEF', trackingNo: ' SF123 ' }), 'SF123')
assert.equal(exports.receiptTrackingNo({ unitType: 'PALLET', unitCode: 'PLT123' }), '') assert.equal(exports.receiptTrackingNo({ unitType: 'PALLET', unitCode: 'PLT123' }), '')
}) })
@@ -18,7 +18,7 @@ export function receiptTrackingNo(receipt: Pick<ReturnReceipt, 'trackingNo' | 'u
const trackingNo = typeof receipt.trackingNo === 'string' ? receipt.trackingNo.trim() : '' const trackingNo = typeof receipt.trackingNo === 'string' ? receipt.trackingNo.trim() : ''
if (trackingNo) return trackingNo if (trackingNo) return trackingNo
const code = typeof receipt.unitCode === 'string' ? receipt.unitCode.trim() : '' const code = typeof receipt.unitCode === 'string' ? receipt.unitCode.trim() : ''
return receipt.unitType === 'PARCEL' && code && !/^PK[0-9A-F]{16}$/.test(code) ? code : '' return receipt.unitType === 'PARCEL' && code && !/^(?:PK[0-9A-F]{16}|PK-[0-9A-Z]+)$/.test(code) ? code : ''
} }
export function formatReturnVolume(value?: number | null) { export function formatReturnVolume(value?: number | null) {
@@ -11,8 +11,8 @@ import CompletionConfirmationDialog from '../components/CompletionConfirmationDi
import { returnRelabelCandidate } from '../utils/returnRelabelCandidates' import { returnRelabelCandidate } from '../utils/returnRelabelCandidates'
import { import {
createDiscard, createRelabel, createTransfer, getDiscard, getRelabel, getTransfer, resubmitRelabel, createDiscard, createRelabel, createTransfer, getDiscard, getRelabel, getTransfer, resubmitRelabel,
getTransferCartonLabel, submitTransfer, updateTransfer, getTransferCartonLabel, submitTransfer, updateTransfer, getTransferSourceCartons, type TransferSourceCarton,
confirmTransferMeasurement, downloadRelabelCompletionPhoto, downloadTransferMeasurementPhoto, downloadDiscardPhoto, type DiscardPhoto, uploadRelabelShippingLabel, downloadRelabelCompletionPhoto, downloadTransferMeasurementPhoto, downloadDiscardPhoto, type DiscardPhoto, uploadRelabelShippingLabel,
uploadRelabelFile, uploadRelabelFile,
listDiscards, listDiscardCandidates, listRelabelInventoryCandidates, listRelabelHistoricalOrders, listRelabels, listTransfers, cancelDiscard, listDiscards, listDiscardCandidates, listRelabelInventoryCandidates, listRelabelHistoricalOrders, listRelabels, listTransfers, cancelDiscard,
type RelabelInventoryCandidate, type RelabelInventoryCandidate,
@@ -57,8 +57,8 @@ const canConfirmTransfer = computed(() => auth.hasPermission('merchant:overseas-
const canConfirmDiscard = computed(() => auth.hasPermission('merchant:overseas-warehouse:discard:confirm')) const canConfirmDiscard = computed(() => auth.hasPermission('merchant:overseas-warehouse:discard:confirm'))
const completionConfirmOpen = ref(false) const completionConfirmOpen = ref(false)
const completionConfirmNo = ref('') const completionConfirmNo = ref('')
const completionConfirmKind = ref<'disposal' | 'labeling'>('disposal') const completionConfirmKind = ref<'disposal' | 'labeling' | 'transfer'>('disposal')
function openCompletionConfirmation(no: string, kind: 'disposal' | 'labeling') { function openCompletionConfirmation(no: string, kind: 'disposal' | 'labeling' | 'transfer') {
if (!no) return if (!no) return
completionConfirmNo.value = no; completionConfirmKind.value = kind; completionConfirmOpen.value = true completionConfirmNo.value = no; completionConfirmKind.value = kind; completionConfirmOpen.value = true
} }
@@ -294,6 +294,22 @@ const availableSkusCollapsed = ref(false)
const relabelCandidatesLoading = ref(false) const relabelCandidatesLoading = ref(false)
const transferInventoryCandidates = ref<RelabelInventoryCandidate[]>([]) const transferInventoryCandidates = ref<RelabelInventoryCandidate[]>([])
const selectedTransferInventory = ref('') const selectedTransferInventory = ref('')
const inventoryTransfer = computed(() => props.service === 'transfer' && Boolean(selectedTransferInventory.value))
const transferSourceCartons = ref<TransferSourceCarton[]>([])
const transferSourceLoading = ref(false)
const transferSourceError = ref('')
let transferSourceRequest = 0
async function loadTransferSourceCartons() {
const request = ++transferSourceRequest
transferSourceCartons.value = []; transferSourceError.value = ''
if (!inventoryTransfer.value) return
transferSourceLoading.value = true
try {
const cartons = await getTransferSourceCartons(form.warehouse, selectedTransferInventory.value)
if (request === transferSourceRequest) transferSourceCartons.value = cartons
} catch (error) { if (request === transferSourceRequest) transferSourceError.value = error instanceof Error ? error.message : '原单箱明细加载失败' }
finally { if (request === transferSourceRequest) transferSourceLoading.value = false }
}
// Snowflake IDs exceed JavaScript's safe integer range. Keep them as strings // Snowflake IDs exceed JavaScript's safe integer range. Keep them as strings
// all the way to JSON so the backend receives the exact source order ID. // all the way to JSON so the backend receives the exact source order ID.
const selectedTransferSourceOrderIds = ref<string[]>([]) const selectedTransferSourceOrderIds = ref<string[]>([])
@@ -545,6 +561,8 @@ async function openEdit(transferNo: string) {
const sourceInboundCode = String(detail.inboundCode || '') const sourceInboundCode = String(detail.inboundCode || '')
if (transferInventoryOptions.value.some((option) => option.value === sourceInboundCode)) { if (transferInventoryOptions.value.some((option) => option.value === sourceInboundCode)) {
selectedTransferInventory.value = sourceInboundCode selectedTransferInventory.value = sourceInboundCode
await loadTransferSourceCartons()
if (transferDestinationType.value === 'NONE') transferDestinationType.value = 'FBA'
form.inboundNo = sourceInboundCode form.inboundNo = sourceInboundCode
selectedTransferSourceOrderIds.value = (detail.sourceOrders || []) selectedTransferSourceOrderIds.value = (detail.sourceOrders || [])
.map((item) => String(item.lastlegOrderId || '')).filter(Boolean) .map((item) => String(item.lastlegOrderId || '')).filter(Boolean)
@@ -1058,7 +1076,7 @@ function transferRelabelStatusTagType(status: string): StatusTagType {
function deliveryMethodLabel(value: string) { function deliveryMethodLabel(value: string) {
const labels: Record<string, string> = { const labels: Record<string, string> = {
TRUCK: '卡车派送', EXPRESS: '快递派送', AMAZON_PICKUP: '亚马逊自提', CUSTOMER_PICKUP: '客户自提', TRUCK: '卡车派送', EXPRESS: '快递派送', AMAZON_PICKUP: '亚马逊自提', CUSTOMER_PICKUP: '客户自提',
WAREHOUSE_PICKUP: '仓库上门提货', CUSTOMER_DELIVERY: '客户送货到仓', INVENTORY_TRANSFER: '库存转运', WAREHOUSE_PICKUP: '仓库上门提货', CUSTOMER_DELIVERY: '客户送货到仓',
// Keep the legacy value readable for historical applications created before the four-way split. // Keep the legacy value readable for historical applications created before the four-way split.
PICKUP: '仓库自提', PICKUP: '仓库自提',
} }
@@ -1160,7 +1178,7 @@ async function submit(action: 'draft' | 'submit' = 'submit') {
if (props.service === 'relabel' && (!form.destination || !form.address || !form.deliveryMethod || !form.estimatedArrivalAt || !form.fbaShipmentNo || form.volumeCbm <= 0)) { if (props.service === 'relabel' && (!form.destination || !form.address || !form.deliveryMethod || !form.estimatedArrivalAt || !form.fbaShipmentNo || form.volumeCbm <= 0)) {
ElMessage.warning('请填写完整的派送信息、体积、预计到达时间和 FBA 货件号'); return ElMessage.warning('请填写完整的派送信息、体积、预计到达时间和 FBA 货件号'); return
} }
if (props.service === 'transfer' && (!form.warehouse || !form.deliveryMethod || !form.address || !form.estimatedArrivalAt || (form.deliveryMethod === 'WAREHOUSE_PICKUP' && (!form.pickupContact || !form.pickupPhone)))) { if (props.service === 'transfer' && !inventoryTransfer.value && (!form.warehouse || !form.deliveryMethod || !form.address || !form.estimatedArrivalAt || (form.deliveryMethod === 'WAREHOUSE_PICKUP' && (!form.pickupContact || !form.pickupPhone)))) {
const missing = [!form.warehouse && '海外仓', !form.deliveryMethod && '送货方式', !form.address && '地址', form.deliveryMethod === 'WAREHOUSE_PICKUP' && !form.pickupContact && '提货联系人', form.deliveryMethod === 'WAREHOUSE_PICKUP' && !form.pickupPhone && '联系人电话', !form.estimatedArrivalAt && '预计入仓时间'].filter(Boolean).join('、') const missing = [!form.warehouse && '海外仓', !form.deliveryMethod && '送货方式', !form.address && '地址', form.deliveryMethod === 'WAREHOUSE_PICKUP' && !form.pickupContact && '提货联系人', form.deliveryMethod === 'WAREHOUSE_PICKUP' && !form.pickupPhone && '联系人电话', !form.estimatedArrivalAt && '预计入仓时间'].filter(Boolean).join('、')
ElMessage.warning(`请填写:${missing || '必填信息'}`); return ElMessage.warning(`请填写:${missing || '必填信息'}`); return
} }
@@ -1168,6 +1186,8 @@ async function submit(action: 'draft' | 'submit' = 'submit') {
!Number.isSafeInteger(row.quantity) || row.quantity < 1 || !Number.isSafeInteger(row.expectedPackageCount) || Number(row.expectedPackageCount) < 1))) { !Number.isSafeInteger(row.quantity) || row.quantity < 1 || !Number.isSafeInteger(row.expectedPackageCount) || Number(row.expectedPackageCount) < 1))) {
ElMessage.warning('请填写每个 SKU 的数量(件)和预计箱数,均须为正整数'); return ElMessage.warning('请填写每个 SKU 的数量(件)和预计箱数,均须为正整数'); return
} }
if (inventoryTransfer.value && (transferSourceLoading.value || transferSourceError.value)) { ElMessage.warning('请等待原单箱明细加载成功后再保存'); return }
if (inventoryTransfer.value && transferDestinationType.value === 'NONE') { ElMessage.warning('库存转运请选择 FBA 仓或私人仓'); return }
if (props.service === 'transfer' && transferDestinationType.value === 'FBA' && !form.destination) { if (props.service === 'transfer' && transferDestinationType.value === 'FBA' && !form.destination) {
ElMessage.warning('请选择 FBA 目的仓'); return ElMessage.warning('请选择 FBA 目的仓'); return
} }
@@ -1181,6 +1201,9 @@ async function submit(action: 'draft' | 'submit' = 'submit') {
if (action === 'submit' && !requiresBothTotalFiles.value && !cartonTotalFile.value && !skuTotalFile.value) { ElMessage.warning('请上传箱唛总文件或 SKU 总文件(二选一)'); return } if (action === 'submit' && !requiresBothTotalFiles.value && !cartonTotalFile.value && !skuTotalFile.value) { ElMessage.warning('请上传箱唛总文件或 SKU 总文件(二选一)'); return }
const relabelRows = transferRelabelRows.value const relabelRows = transferRelabelRows.value
const allMarks = relabelRows.flatMap(transferCustomerMarks) const allMarks = relabelRows.flatMap(transferCustomerMarks)
if (inventoryTransfer.value && allMarks.some(mark => !transferSourceCartons.value.some(box => box.cartonMark === mark))) {
ElMessage.error('当前箱子不存在或不属于当前订单'); return
}
if (!relabelRows.length || relabelRows.some((row) => transferCustomerMarks(row).length !== row.quantity)) { ElMessage.warning('每个换标明细的客户箱唛数量必须与换标数量一致'); return } if (!relabelRows.length || relabelRows.some((row) => transferCustomerMarks(row).length !== row.quantity)) { ElMessage.warning('每个换标明细的客户箱唛数量必须与换标数量一致'); return }
if (new Set(allMarks.map((mark) => mark.toUpperCase())).size !== allMarks.length) { ElMessage.warning('客户箱唛号不能重复'); return } if (new Set(allMarks.map((mark) => mark.toUpperCase())).size !== allMarks.length) { ElMessage.warning('客户箱唛号不能重复'); return }
if (allMarks.join(',').length > 2000 || relabelRows.some((row) => transferCustomerMarks(row).join(',').length > 255)) { ElMessage.warning('客户箱唛内容过长,请减少单次提交箱数或缩短箱唛号'); return } if (allMarks.join(',').length > 2000 || relabelRows.some((row) => transferCustomerMarks(row).join(',').length > 255)) { ElMessage.warning('客户箱唛内容过长,请减少单次提交箱数或缩短箱唛号'); return }
@@ -1232,7 +1255,7 @@ async function submit(action: 'draft' | 'submit' = 'submit') {
const transferRemark = [form.remark, form.relabel && form.requirement ? `换标要求:${form.requirement}` : ''].filter(Boolean).join(';') const transferRemark = [form.remark, form.relabel && form.requirement ? `换标要求:${form.requirement}` : ''].filter(Boolean).join(';')
const operationType = labelingMode.value === 'carton' ? 'CARTON_LABEL' : labelingMode.value === 'sku' ? 'SKU_LABEL' : 'CARTON_AND_SKU' const operationType = labelingMode.value === 'carton' ? 'CARTON_LABEL' : labelingMode.value === 'sku' ? 'SKU_LABEL' : 'CARTON_AND_SKU'
const selectedFba = fbaWarehouses.value.find((warehouse) => warehouse.warehouseCode === form.destination) const selectedFba = fbaWarehouses.value.find((warehouse) => warehouse.warehouseCode === form.destination)
const payload: TransferCreate = { operatingWarehouseId: primaryWarehouse, destinationType: transferDestinationType.value, fbaWarehouseCode: transferDestinationType.value === 'FBA' ? form.destination || undefined : undefined, destinationWarehouseId: transferDestinationType.value === 'FBA' ? String(selectedFba?.id || '') || undefined : undefined, destinationWarehouseCode: transferDestinationType.value === 'FBA' ? form.destination || undefined : undefined, destinationWarehouseName: transferDestinationType.value === 'FBA' ? selectedFba?.warehouseName : undefined, privateDestinationAddress: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationAddress : undefined, privateDestinationPostalCode: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationPostalCode : undefined, privateDestinationContactName: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationContact : undefined, privateDestinationContactPhone: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationPhone : undefined, deliveryAddress: form.address, packageCount, volumeCbm: 0, deliveryMethod: form.deliveryMethod, estimatedArrivalAt: form.estimatedArrivalAt, estimatedShipAt: form.estimatedShipAt || undefined, fbaShipmentNo: transferDestinationType.value === 'FBA' ? form.fbaShipmentNo || undefined : undefined, trackingNo: form.deliveryMethod === 'CUSTOMER_DELIVERY' ? form.trackingNo || undefined : undefined, fbaBoxNos: customerCartonMarks.length ? customerCartonMarks.join(',') : undefined, remark: transferRemark || undefined, needRelabel: props.service === 'relabel' || form.relabel, labelDeadline: form.relabel ? form.labelDeadline : undefined, pickupContactName: form.deliveryMethod === 'WAREHOUSE_PICKUP' ? form.pickupContact : undefined, pickupContactPhone: form.deliveryMethod === 'WAREHOUSE_PICKUP' ? form.pickupPhone : undefined, relabelContactName: form.relabel ? form.relabelContact : undefined, relabelContactPhone: form.relabel ? form.relabelPhone : undefined, relabelDeliveryMethod: form.relabel && labelingMode.value !== 'sku' ? form.relabelDeliveryMethod as 'TRUCK' | 'EXPRESS' | 'AMAZON_PICKUP' | 'CUSTOMER_PICKUP' : undefined, idempotencyKey, items: cargoRows.value.map((item) => ({ skuCode: item.sku, productNameZh: item.name, quantity: item.quantity, packageCount: Number(item.expectedPackageCount), shippingMark: transferCustomerMarks(item).join(',') || undefined })), relabelItems: form.relabel ? transferRelabelRows.value.map((item) => ({ operationType, customerCartonMarks: transferCustomerMarks(item), originalSku: item.sku, newSku: item.newSkuCode || undefined, quantity: item.quantity, fbaCartonMark: item.fbaCartonMark || undefined, destinationWarehouseCode: transferRelabelDefaultDestination.value || undefined, remark: item.remark || undefined })) : undefined } const payload: TransferCreate = { operatingWarehouseId: primaryWarehouse, destinationType: transferDestinationType.value, fbaWarehouseCode: transferDestinationType.value === 'FBA' ? form.destination || undefined : undefined, destinationWarehouseId: transferDestinationType.value === 'FBA' ? String(selectedFba?.id || '') || undefined : undefined, destinationWarehouseCode: transferDestinationType.value === 'FBA' ? form.destination || undefined : undefined, destinationWarehouseName: transferDestinationType.value === 'FBA' ? selectedFba?.warehouseName : undefined, privateDestinationAddress: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationAddress : undefined, privateDestinationPostalCode: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationPostalCode : undefined, privateDestinationContactName: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationContact : undefined, privateDestinationContactPhone: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationPhone : undefined, deliveryAddress: inventoryTransfer.value ? undefined : form.address, packageCount, volumeCbm: 0, deliveryMethod: inventoryTransfer.value ? undefined : form.deliveryMethod, estimatedArrivalAt: inventoryTransfer.value ? undefined : form.estimatedArrivalAt, estimatedShipAt: form.estimatedShipAt || undefined, fbaShipmentNo: transferDestinationType.value === 'FBA' ? form.fbaShipmentNo || undefined : undefined, trackingNo: !inventoryTransfer.value && form.deliveryMethod === 'CUSTOMER_DELIVERY' ? form.trackingNo || undefined : undefined, fbaBoxNos: customerCartonMarks.length ? customerCartonMarks.join(',') : undefined, remark: transferRemark || undefined, needRelabel: props.service === 'relabel' || form.relabel, labelDeadline: form.relabel ? form.labelDeadline : undefined, pickupContactName: !inventoryTransfer.value && form.deliveryMethod === 'WAREHOUSE_PICKUP' ? form.pickupContact : undefined, pickupContactPhone: !inventoryTransfer.value && form.deliveryMethod === 'WAREHOUSE_PICKUP' ? form.pickupPhone : undefined, relabelContactName: form.relabel ? form.relabelContact : undefined, relabelContactPhone: form.relabel ? form.relabelPhone : undefined, relabelDeliveryMethod: form.relabel && labelingMode.value !== 'sku' ? form.relabelDeliveryMethod as 'TRUCK' | 'EXPRESS' | 'AMAZON_PICKUP' | 'CUSTOMER_PICKUP' : undefined, idempotencyKey, items: cargoRows.value.map((item) => ({ skuCode: item.sku, productNameZh: item.name, quantity: item.quantity, packageCount: Number(item.expectedPackageCount), shippingMark: transferCustomerMarks(item).join(',') || undefined })), relabelItems: form.relabel ? transferRelabelRows.value.map((item) => ({ operationType, customerCartonMarks: transferCustomerMarks(item), originalSku: item.sku, newSku: item.newSkuCode || undefined, quantity: item.quantity, fbaCartonMark: item.fbaCartonMark || undefined, destinationWarehouseCode: transferRelabelDefaultDestination.value || undefined, remark: item.remark || undefined })) : undefined }
if (props.service === 'transfer' && selectedTransferInventory.value) { if (props.service === 'transfer' && selectedTransferInventory.value) {
;(payload as TransferCreate).inboundCode = selectedTransferInventory.value ;(payload as TransferCreate).inboundCode = selectedTransferInventory.value
;(payload as TransferCreate).sourceLastlegOrderIds = selectedTransferSourceOrderIds.value ;(payload as TransferCreate).sourceLastlegOrderIds = selectedTransferSourceOrderIds.value
@@ -1297,9 +1320,7 @@ async function loadDetailPage() {
async function confirmTransferMeasurements() { async function confirmTransferMeasurements() {
const transferNo = String(selectedDetail.value?.transferNo || selectedDetail.value?.['0'] || '') const transferNo = String(selectedDetail.value?.transferNo || selectedDetail.value?.['0'] || '')
if (!transferNo) return if (!transferNo) return
try { await ElMessageBox.confirm('请核对 PDA 回传的箱数、尺寸、重量和打托数据。', '确认测量数据', { confirmButtonText: '确认数据', cancelButtonText: '取消' }) } catch { return } openCompletionConfirmation(transferNo, 'transfer')
try { selectedDetail.value = await confirmTransferMeasurement(transferNo) as ServiceDetail; ElMessage.success('仓库实测数据已确认') }
catch (error) { ElMessage.error(error instanceof Error ? error.message : '确认失败') }
} }
async function printTransferCartons(transferNo: string) { async function printTransferCartons(transferNo: string) {
if (props.service !== 'transfer') return if (props.service !== 'transfer') return
@@ -1320,9 +1341,7 @@ function clearCompletionPhotos() {
for (const key of Object.keys(completionPhotoErrors)) delete completionPhotoErrors[key] for (const key of Object.keys(completionPhotoErrors)) delete completionPhotoErrors[key]
} }
async function confirmTransferFromList(transferNo: string) { async function confirmTransferFromList(transferNo: string) {
try { await ElMessageBox.confirm('请核对 PDA 回传的箱数、尺寸、重量和打托数据。', '确认测量数据', { confirmButtonText: '确认数据', cancelButtonText: '取消' }) } catch { return } openCompletionConfirmation(transferNo, 'transfer')
try { await confirmTransferMeasurement(transferNo); ElMessage.success('仓库实测数据已确认'); await load() }
catch (error) { ElMessage.error(error instanceof Error ? error.message : '确认失败') }
} }
async function loadCompletionPhoto(fileId: string) { async function loadCompletionPhoto(fileId: string) {
const relabelNo = selectedRelabelDetail.value?.application.relabelNo const relabelNo = selectedRelabelDetail.value?.application.relabelNo
@@ -1476,7 +1495,10 @@ const transferInventoryOptions = computed(() => {
async function selectTransferInventory(value: string) { async function selectTransferInventory(value: string) {
selectedTransferInventory.value = value || '' selectedTransferInventory.value = value || ''
selectedTransferSourceOrderIds.value = [] selectedTransferSourceOrderIds.value = []
if (!value) return transferSourceCartons.value = []; transferSourceError.value = ''; ++transferSourceRequest
if (!value) { form.inboundNo = ''; cargoRows.value = [createCargoRow()]; transferSourceLoading.value = false; return }
if (transferDestinationType.value === 'NONE') transferDestinationType.value = 'FBA'
void loadTransferSourceCartons()
form.inboundNo = value form.inboundNo = value
// The inventory-candidates response already contains the merchant-visible // The inventory-candidates response already contains the merchant-visible
// SKU/name/package data. Do not call the platform-only last-leg orders API // SKU/name/package data. Do not call the platform-only last-leg orders API
@@ -1489,14 +1511,14 @@ async function selectTransferInventory(value: string) {
const sku = String(candidate.skuCode || '') const sku = String(candidate.skuCode || '')
const current = grouped.get(sku) const current = grouped.get(sku)
const quantity = inventoryQuantity(candidate.availablePackages) const quantity = inventoryQuantity(candidate.availablePackages)
if (current) current.quantity += quantity if (current) { current.quantity += quantity; current.expectedPackageCount = Number(current.expectedPackageCount || 0) + quantity }
else grouped.set(sku, { ...createCargoRow(), sku, name: candidate.productNameZh || '', quantity, availableQuantity: quantity }) else grouped.set(sku, { ...createCargoRow(), sku, name: candidate.productNameZh || '', quantity, expectedPackageCount: quantity, availableQuantity: quantity })
} }
cargoRows.value = [...grouped.values()] cargoRows.value = [...grouped.values()]
try { try {
const historical = await listRelabelHistoricalOrders(form.warehouse, value) const historical = await listRelabelHistoricalOrders(form.warehouse, value)
const sourceIds = historical.map((item) => String(item.id || '')).filter(Boolean) const sourceIds = historical.map((item) => String(item.id || '')).filter(Boolean)
selectedTransferSourceOrderIds.value = sourceIds if (selectedTransferInventory.value === value) selectedTransferSourceOrderIds.value = sourceIds
} catch (error) { } catch (error) {
ElMessage.error(error instanceof Error ? error.message : '库存来源订单加载失败') ElMessage.error(error instanceof Error ? error.message : '库存来源订单加载失败')
} }
@@ -1615,11 +1637,11 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
<label v-if="props.service === 'labeling'" class="warehouse-span-2"><span><i>*</i>选择入仓单或退货单</span><el-select v-model="selectedRelabelInboundOrderId" filterable clearable :filter-method="(value: string) => relabelInventorySearch = value" :loading="relabelCandidatesLoading" :disabled="!form.warehouse" no-data-text="暂无匹配的可选库存" placeholder="搜索单号或选择可用库存" @change="selectRelabelInventory"><el-option v-for="option in visibleRelabelInboundOptions.filter(item => !relabelInventorySearch.trim() || item.inboundOrderNo.toUpperCase().includes(relabelInventorySearch.trim().toUpperCase()) || item.inboundCode.toUpperCase().includes(relabelInventorySearch.trim().toUpperCase()))" :key="option.value" :label="relabelOptionLabel(option)" :value="option.value" :disabled="option.availablePackages <= 0" /></el-select><small 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 :filter-method="(value: string) => relabelInventorySearch = value" :loading="relabelCandidatesLoading" :disabled="!form.warehouse" no-data-text="暂无匹配的可选库存" placeholder="搜索单号或选择可用库存" @change="selectRelabelInventory"><el-option v-for="option in visibleRelabelInboundOptions.filter(item => !relabelInventorySearch.trim() || item.inboundOrderNo.toUpperCase().includes(relabelInventorySearch.trim().toUpperCase()) || item.inboundCode.toUpperCase().includes(relabelInventorySearch.trim().toUpperCase()))" :key="option.value" :label="relabelOptionLabel(option)" :value="option.value" :disabled="option.availablePackages <= 0" /></el-select><small class="warehouse-hint">在仓数量包含被占用货物;本次可选数量才可加入申请。合箱后的库存按当前包裹/箱展示,来源退货单保留。</small></label>
<label v-if="props.service === 'transfer'"><span>库存转运来源(可选)</span><el-select v-model="selectedTransferInventory" filterable clearable :loading="relabelCandidatesLoading" :disabled="!form.warehouse" no-data-text="当前海外仓暂无可用库存" placeholder="不选则按新货转运;选择后免 PDA 测量" @change="selectTransferInventory"><el-option v-for="option in transferInventoryOptions" :key="option.value" :label="`${option.label}|可用 ${option.packages} 箱`" :value="option.value" /></el-select><small class="warehouse-hint">库存转运可直接进入目的仓、换标和排车流程,PDA 可拆托、重打或同目的仓合托。</small></label> <label v-if="props.service === 'transfer'"><span>库存转运来源(可选)</span><el-select v-model="selectedTransferInventory" filterable clearable :loading="relabelCandidatesLoading" :disabled="!form.warehouse" no-data-text="当前海外仓暂无可用库存" placeholder="不选则按新货转运;选择后免 PDA 测量" @change="selectTransferInventory"><el-option v-for="option in transferInventoryOptions" :key="option.value" :label="`${option.label}|可用 ${option.packages} 箱`" :value="option.value" /></el-select><small class="warehouse-hint">库存转运可直接进入目的仓、换标和排车流程,PDA 可拆托、重打或同目的仓合托。</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></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></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> <div v-if="props.service === 'transfer' && !inventoryTransfer" 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' && !inventoryTransfer && 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' && !inventoryTransfer && 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> <label v-if="props.service === 'transfer' && !inventoryTransfer && form.deliveryMethod === 'WAREHOUSE_PICKUP'"><span><i>*</i>提货联系人电话</span><el-input v-model="form.pickupPhone" placeholder="请输入提货联系人电话" maxlength="64" /></label>
<div v-if="props.service === 'transfer' && form.deliveryMethod === 'CUSTOMER_DELIVERY'" v-loading="operationWarehouseDetailLoading" class="warehouse-receiving-card warehouse-span-2"> <div v-if="props.service === 'transfer' && !inventoryTransfer && form.deliveryMethod === 'CUSTOMER_DELIVERY'" v-loading="operationWarehouseDetailLoading" class="warehouse-receiving-card warehouse-span-2">
<div><span>海外仓收货信息</span><strong>{{ operationWarehouseAddress || '该海外仓暂未维护收货地址' }}</strong></div> <div><span>海外仓收货信息</span><strong>{{ operationWarehouseAddress || '该海外仓暂未维护收货地址' }}</strong></div>
<label class="warehouse-span-2"><span><i>*</i>送货地址</span><el-input v-model="form.address" placeholder="请输入实际送货地址" /></label> <label class="warehouse-span-2"><span><i>*</i>送货地址</span><el-input v-model="form.address" placeholder="请输入实际送货地址" /></label>
<div><span>收货联系人</span><strong>{{ selectedOperationWarehouse?.warehouse.contactName || '未维护' }}</strong></div> <div><span>收货联系人</span><strong>{{ selectedOperationWarehouse?.warehouse.contactName || '未维护' }}</strong></div>
@@ -1636,9 +1658,9 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
<label v-if="canManageRelabelFiles && props.service === 'labeling' && labelingMode !== 'sku' && form.deliveryMethod === 'EXPRESS' && form.relabel"><span><i>*</i>上传面单文件</span><input class="warehouse-native-file" type="file" accept="application/pdf,image/*,.pdf" @change="selectShippingLabel"></label> <label v-if="canManageRelabelFiles && props.service === 'labeling' && labelingMode !== 'sku' && form.deliveryMethod === 'EXPRESS' && 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 === 'relabel'" class="warehouse-span-2"><span><i>*</i>详细派送地址</span><el-input v-model="form.address" placeholder="请输入派送地址" /></label>
<label v-if="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 === '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' ? '预计入仓时间' : '预计到达时间' }}</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' && !inventoryTransfer) || 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="选填" /></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>目的去向</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> <div v-if="props.service === 'transfer'" class="warehouse-choice-field warehouse-span-2"><span>目的去向</span><el-radio-group v-model="transferDestinationType" @change="selectTransferDestinationType"><el-radio-button v-if="!inventoryTransfer" 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><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 === 'FBA'"><span>FBA 货件编号</span><el-input v-model="form.fbaShipmentNo" placeholder="选填,可后续补充" /></label>
<label v-if="props.service === 'transfer' && transferDestinationType === 'PRIVATE'"><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.privateDestinationAddress" maxlength="1000" placeholder="请输入私人仓完整地址" /></label>
@@ -1719,8 +1741,17 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
</template> </template>
<el-alert v-else type="info" :closable="false" :title="discardSelectedCandidate.cartonSelectionBlockReason || '当前库存缺少完整箱级记录,暂时只能选择全部库存废弃。'" /> <el-alert v-else type="info" :closable="false" :title="discardSelectedCandidate.cartonSelectionBlockReason || '当前库存缺少完整箱级记录,暂时只能选择全部库存废弃。'" />
</section> </section>
<div v-if="props.service !== 'disposal' || !discardSelectedCandidate?.cartonSelectionSupported" 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() && Number(row.expectedPackageCount) > 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> <section v-if="inventoryTransfer" v-loading="transferSourceLoading">
<div v-if="props.service !== 'disposal' || !discardSelectedCandidate?.cartonSelectionSupported" 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 v-if="props.service === 'transfer'">SKU 数量(件)*</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 v-if="props.service === 'transfer'"><el-input-number v-model="row.quantity" :min="1" :precision="0" controls-position="right" placeholder="请输入件数" /></td><td v-if="props.service === 'transfer'"><el-input-number v-model="row.expectedPackageCount" :min="1" :precision="0" controls-position="right" placeholder="请输入预计箱数" /></td><td v-else><el-input-number v-model="row.quantity" :disabled="props.service === 'disposal'" :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' || props.service === 'transfer' ? 5 : 4"><div class="warehouse-empty">{{ props.service === 'disposal' ? '请先选择入库单,系统将自动带出 SKU 和可用箱数' : '暂无货物明细,请点击“添加行”' }}</div></td></tr></tbody></table></div> <div class="warehouse-section-heading"><h3>原单箱明细 <small>共 {{ transferSourceCartons.length }} 箱 · 只读</small></h3></div>
<el-alert v-if="transferSourceError" :title="transferSourceError" type="error" :closable="false"><el-button link @click="loadTransferSourceCartons">重新加载</el-button></el-alert>
<div class="warehouse-table-scroll"><table class="warehouse-edit-table"><thead><tr><th>箱号</th><th>商品 SKU</th><th>中文品名</th><th>箱内数量(件)</th><th>原目的仓</th></tr></thead><tbody>
<tr v-for="box in transferSourceCartons" :key="box.cartonMark"><td>{{ box.cartonMark }}</td><td>{{ box.items.map(item => item.skuCode || '—').join(' / ') }}</td><td>{{ box.productNameZh || '—' }}</td><td>{{ box.quantity ?? '未记录' }}</td><td>{{ [box.destinationWarehouseCode, box.destinationWarehouseName].filter(Boolean).join(' · ') || '未指定' }}</td></tr>
<tr v-if="!transferSourceLoading && !transferSourceCartons.length"><td colspan="5"><div class="warehouse-empty">{{ transferSourceError ? '加载失败,请重试' : '原库存单据未记录逐箱明细,不虚构箱号;按原库存汇总转运' }}</div></td></tr>
</tbody></table></div>
<div v-if="!transferSourceLoading && !transferSourceError && !transferSourceCartons.length" 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="row in cargoRows" :key="row.id"><td>{{ row.sku || '—' }}</td><td>{{ row.name || '—' }}</td><td>{{ row.expectedPackageCount }}</td><td>{{ transferInventoryCandidates.filter(item => item.inboundCode === selectedTransferInventory && item.skuCode === row.sku).map(item => item.destinationFbaWarehouseCode || item.destinationFbaWarehouseName).filter(Boolean).join(' / ') || '未指定' }}</td></tr></tbody></table></div>
</section>
<div v-if="!inventoryTransfer && (props.service !== 'disposal' || !discardSelectedCandidate?.cartonSelectionSupported)" 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() && Number(row.expectedPackageCount) > 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 v-if="!inventoryTransfer && (props.service !== 'disposal' || !discardSelectedCandidate?.cartonSelectionSupported)" 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 v-if="props.service === 'transfer'">SKU 数量(件)*</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 v-if="props.service === 'transfer'"><el-input-number v-model="row.quantity" :min="1" :precision="0" controls-position="right" placeholder="请输入件数" /></td><td v-if="props.service === 'transfer'"><el-input-number v-model="row.expectedPackageCount" :min="1" :precision="0" controls-position="right" placeholder="请输入预计箱数" /></td><td v-else><el-input-number v-model="row.quantity" :disabled="props.service === 'disposal'" :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' || props.service === 'transfer' ? 5 : 4"><div class="warehouse-empty">{{ props.service === 'disposal' ? '请先选择入库单,系统将自动带出 SKU 和可用箱数' : '暂无货物明细,请点击“添加行”' }}</div></td></tr></tbody></table></div>
</template> </template>
</div> </div>
<div v-if="props.service === 'transfer' && form.relabel" class="warehouse-card warehouse-form-section warehouse-transfer-relabel"> <div v-if="props.service === 'transfer' && form.relabel" class="warehouse-card warehouse-form-section warehouse-transfer-relabel">
@@ -0,0 +1,41 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { transpileModule, ScriptTarget } from 'typescript'
const view = readFileSync(new URL('./WarehouseServiceView.vue', import.meta.url), 'utf8')
function setup() {
const body = view.slice(view.indexOf('async function selectTransferInventory('), view.indexOf('function selectTransferInboundMethod('))
const code = transpileModule(body, { compilerOptions: { target: ScriptTarget.ES2022 } }).outputText
const ctx = { selectedTransferInventory: {value:''}, selectedTransferSourceOrderIds:{value:[]},
transferSourceCartons:{value:[{cartonMark:'OLD'}]}, transferSourceError:{value:'OLD'}, transferSourceLoading:{value:true},
transferDestinationType:{value:'NONE'}, form:{warehouse:'WH',inboundNo:''},
transferInventoryCandidates:{value:[{inboundCode:'IN',skuCode:'SKU',productNameZh:'货品',availablePackages:2},
{inboundCode:'IN',skuCode:'SKU',availablePackages:3},{inboundCode:'OTHER',skuCode:'OTHER',availablePackages:99}]},
cargoRows:{value:[]}, inventoryQuantity:Number, createCargoRow:()=>({expectedPackageCount:1}),
loadTransferSourceCartons:async()=>{}, listRelabelHistoricalOrders:async()=>[{id:'1234567890123456789'}],
ElMessage:{error(message){throw Error(message)}} }
const select = new Function(...Object.keys(ctx), 'let transferSourceRequest=0;'+code+'; return selectTransferInventory')(...Object.values(ctx))
return {ctx,select}
}
test('inventory source populates full immutable cargo counts without mixing other inbound orders',async()=>{
const {ctx,select}=setup(); await select('IN')
assert.equal(ctx.transferDestinationType.value,'FBA')
assert.equal(ctx.cargoRows.value.length,1)
assert.equal(ctx.cargoRows.value[0].expectedPackageCount,5)
assert.equal(ctx.cargoRows.value[0].quantity,5)
assert.deepEqual(ctx.selectedTransferSourceOrderIds.value,['1234567890123456789'])
await select('')
assert.equal(ctx.form.inboundNo,'')
assert.deepEqual(ctx.selectedTransferSourceOrderIds.value,[])
assert.deepEqual(ctx.transferSourceCartons.value,[])
})
test('inventory view hides inbound fields and editing actions and shows original carton destination',()=>{
assert.match(view,/v-if="props.service === 'transfer' && !inventoryTransfer"[^>]*><span><i>\*<\/i>入仓方式/)
assert.match(view,/<el-radio-button v-if="!inventoryTransfer" value="NONE">/)
const panel=view.slice(view.indexOf('<section v-if="inventoryTransfer"'),view.indexOf('</section>',view.indexOf('<section v-if="inventoryTransfer"')))
assert.match(panel,/box.cartonMark/); assert.match(panel,/box.destinationWarehouseCode/)
assert.doesNotMatch(panel,/v-model|addCargoRow|removeCargoRow|清空/)
assert.match(view,/!inventoryTransfer\.value && \(!form.warehouse/)
assert.match(view,/estimatedArrivalAt: inventoryTransfer.value \? undefined : form.estimatedArrivalAt/)
})
@@ -146,7 +146,7 @@ test('仓储转运换标总文件与明细模板在桌面端左右排列', () =>
}) })
test('上门提货不提交物流号,需要换标时随转运单登记申请', () => { test('上门提货不提交物流号,需要换标时随转运单登记申请', () => {
assert.match(view, /trackingNo:\s*form\.deliveryMethod === 'CUSTOMER_DELIVERY'\s*\?[^:]+:\s*undefined/) assert.match(view, /trackingNo:\s*!inventoryTransfer\.value && form\.deliveryMethod === 'CUSTOMER_DELIVERY'\s*\?[^:]+:\s*undefined/)
assert.match(view, /relabelItems: form\.relabel/) assert.match(view, /relabelItems: form\.relabel/)
assert.match(view, /form\.deliveryMethod === 'CUSTOMER_DELIVERY'.*物流跟踪号/s) assert.match(view, /form\.deliveryMethod === 'CUSTOMER_DELIVERY'.*物流跟踪号/s)
assert.match(view, /提货联系人/) assert.match(view, /提货联系人/)
@@ -157,8 +157,8 @@ test('上门提货不提交物流号,需要换标时随转运单登记申请',
assert.match(view, /form\.relabelContact/) assert.match(view, /form\.relabelContact/)
assert.match(view, /form\.relabelPhone/) assert.match(view, /form\.relabelPhone/)
assert.match(view, /选择海外仓后自动带出,也可手动填写/) assert.match(view, /选择海外仓后自动带出,也可手动填写/)
assert.match(view, /pickupContactName: form\.deliveryMethod === 'WAREHOUSE_PICKUP' \? form\.pickupContact : undefined/) assert.match(view, /pickupContactName: !inventoryTransfer\.value && form\.deliveryMethod === 'WAREHOUSE_PICKUP' \? form\.pickupContact : undefined/)
assert.match(view, /pickupContactPhone: form\.deliveryMethod === 'WAREHOUSE_PICKUP' \? form\.pickupPhone : undefined/) assert.match(view, /pickupContactPhone: !inventoryTransfer\.value && form\.deliveryMethod === 'WAREHOUSE_PICKUP' \? form\.pickupPhone : undefined/)
assert.match(view, /relabelContactName: form\.relabel \? form\.relabelContact/) assert.match(view, /relabelContactName: form\.relabel \? form\.relabelContact/)
assert.match(api, /pickupContactName\?: string; pickupContactPhone\?: string/) assert.match(api, /pickupContactName\?: string; pickupContactPhone\?: string/)
}) })