优化样式

This commit is contained in:
2026-10-19 14:30:52 +08:00
parent 7e68d978dd
commit f7dd39c7ed
5 changed files with 136 additions and 33 deletions
@@ -1,11 +1,13 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { Package, Truck } from '@lucide/vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
import type { TableAction } from '@/components/common/tableActions'
import { useAuthStore } from '@/modules/auth/public'
import { getMerchantWarehouses, type MerchantWarehouse } from '@/modules/first-leg/public'
import {
createTransport,
getBusinessOrder,
@@ -52,6 +54,8 @@ interface BusinessForm {
contactInfo: string
requestedDate: string
remark: string
cargoName: string
deliveryDate: string
}
const configuration = computed(() => ({
@@ -94,6 +98,10 @@ const fleetArrangement = computed(() => detail.value ? (detail.value as Transpor
const formRef = ref<FormInstance>()
const query = reactive({ page: 1, size: 20, orderNo: '', routeKeyword: '', status: '' })
const total = ref(0)
const overseasWarehouses = ref<MerchantWarehouse[]>([])
const overseasWarehousesLoading = ref(false)
const overseasWarehousesError = ref('')
const isSelfDelivery = computed(() => props.kind === 'looseboard' && form.transportMode === 'SHARED_TRUCK')
const queryModel = computed<QueryModel>({
get: () => ({
@@ -174,11 +182,30 @@ function emptyForm(): BusinessForm {
transportMode: 'SHARED_TRUCK', palletRequired: 'NO', transportType: 'PORT_PICKUP', origin: '', originAddress: '',
destination: '', destinationAddress: '', containerType: DEFAULT_CONTAINER_TYPE, palletCount: 1, packageCount: 0,
totalWeightKg: 0, totalVolumeCbm: 0, currency: 'USD', contactName: '', contactInfo: '', requestedDate: '',
remark: '',
remark: '', cargoName: '', deliveryDate: '',
}
}
function resetForm() { Object.assign(form, emptyForm()); submitKey.value = ''; formRef.value?.clearValidate() }
function warehouseLabel(warehouse: MerchantWarehouse) { return `${warehouse.warehouseName} · ${warehouse.warehouseCode}` }
function selectOriginMode() {
form.origin = ''
form.originAddress = ''
formRef.value?.clearValidate('origin')
}
async function loadOverseasWarehouses() {
if (props.kind !== 'looseboard') return
overseasWarehousesLoading.value = true
overseasWarehousesError.value = ''
try {
const result = await getMerchantWarehouses({ page: 1, size: 200, warehouseType: 'OVERSEAS' })
overseasWarehouses.value = result.records.filter(item => item.status === 'ACTIVE' && ['OVERSEAS', 'OVERSEAS_PARTNER'].includes(item.warehouseType) && item.countryCode === 'US')
if (!overseasWarehouses.value.length) overseasWarehousesError.value = '暂无可用的海外仓'
} catch (cause) {
overseasWarehouses.value = []
overseasWarehousesError.value = errorMessage(cause)
} finally { overseasWarehousesLoading.value = false }
}
function read(source: object, key: string) { return Reflect.get(source, key) as unknown }
function text(value: unknown) { return value === null || value === undefined || value === '' ? '—' : String(value) }
function money(amount: unknown, currency: unknown) { return amount === null || amount === undefined ? '—' : `${text(currency)} ${Number(amount).toFixed(2)}` }
@@ -289,7 +316,7 @@ async function submit() {
totalWeightKg: form.totalWeightKg, totalVolumeCbm: form.totalVolumeCbm,
budgetAmount: form.budgetAmount, currency: form.currency || undefined,
contactName: form.contactName || undefined, contactInfo: form.contactInfo || undefined,
requestedDate: form.requestedDate || undefined, remark: form.remark || undefined,
requestedDate: form.requestedDate || undefined, remark: [form.cargoName, form.remark].filter(Boolean).join(' · ') || undefined,
idempotencyKey: stableSubmitKey,
}
await createTransport(props.kind, props.kind === 'trucking'
@@ -304,7 +331,7 @@ async function submit() {
}
watch(() => [query.page, query.size], () => { if (!props.createPage && mode.value === 'list') void load() })
onMounted(() => { if (props.detailPage) void loadDetail(); else if (!props.createPage) void load() })
onMounted(() => { if (props.detailPage) void loadDetail(); else if (!props.createPage) void load(); void loadOverseasWarehouses() })
</script>
<template>
@@ -362,26 +389,39 @@ onMounted(() => { if (props.detailPage) void loadDetail(); else if (!props.creat
</section>
</section>
<el-card v-else shadow="never" class="form-card">
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-form-item v-if="kind === 'looseboard'" label="运输方式"><el-radio-group v-model="form.transportMode"><el-radio-button value="SHARED_TRUCK">拼车</el-radio-button><el-radio-button value="PRIORITY_CONSOLIDATION">优先拼载</el-radio-button></el-radio-group></el-form-item>
<el-form-item v-if="kind === 'looseboard'" label="是否打盘"><el-radio-group v-model="form.palletRequired"><el-radio-button value="NO">否</el-radio-button><el-radio-button value="YES">是</el-radio-button></el-radio-group></el-form-item>
<el-form-item label="运输类型"><el-select v-model="form.transportType"><el-option label="港口提柜" value="PORT_PICKUP" /><el-option label="仓到仓" value="WAREHOUSE_TO_WAREHOUSE" /><el-option label="仓到门" value="WAREHOUSE_TO_DOOR" /></el-select></el-form-item>
<el-form-item label="起运地" prop="origin"><el-input v-model.trim="form.origin" maxlength="255" placeholder="请输入起运地" /></el-form-item>
<el-form-item label="起运地址"><el-input v-model.trim="form.originAddress" maxlength="500" placeholder="请输入详细起运地址" /></el-form-item>
<el-form-item label="目的地" prop="destination"><el-input v-model.trim="form.destination" maxlength="255" placeholder="请输入目的地" /></el-form-item>
<el-form-item label="目的地址"><el-input v-model.trim="form.destinationAddress" maxlength="500" placeholder="请输入详细目的地址" /></el-form-item>
<el-form-item label="柜型"><el-select v-model="form.containerType"><el-option v-for="item in CONTAINER_SPECIFICATIONS" :key="item.type" :label="item.type" :value="item.type" /></el-select></el-form-item>
<el-form-item v-if="kind === 'trucking'" label="托盘数" prop="palletCount"><el-input-number v-model="form.palletCount" :min="1" placeholder="请输入托盘数" /></el-form-item>
<el-form-item label="件数"><el-input-number v-model="form.packageCount" :min="0" placeholder="请输入件数" /></el-form-item>
<el-form-item label="总重量(kg)"><el-input-number v-model="form.totalWeightKg" :min="0" :precision="2" placeholder="请输入总重量" /></el-form-item>
<el-form-item label="总体积(m³)"><el-input-number v-model="form.totalVolumeCbm" :min="0" :precision="3" placeholder="请输入总体积" /></el-form-item>
<el-form-item label="要求时间" :prop="kind === 'trucking' ? 'requestedDate' : undefined"><el-date-picker v-model="form.requestedDate" type="date" value-format="YYYY-MM-DD" /></el-form-item>
<el-form-item class="budget-form-item" label="预算金额"><el-input-number v-model="form.budgetAmount" :min="0" :precision="2" placeholder="请输入预算金额" /><el-select v-model="form.currency" class="currency"><el-option label="USD" value="USD" /><el-option label="CNY" value="CNY" /></el-select></el-form-item>
<el-form-item label="联系人" prop="contactName"><el-input v-model.trim="form.contactName" maxlength="128" placeholder="请输入联系人" /></el-form-item>
<el-form-item label="联系方式" prop="contactInfo"><el-input v-model.trim="form.contactInfo" maxlength="128" placeholder="请输入电话、微信或邮箱" /></el-form-item>
<el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="4" maxlength="1000" show-word-limit placeholder="请输入其他要求" /></el-form-item>
<el-form-item><el-button @click="returnToList">取消</el-button><el-button v-if="canCreate" type="primary" :loading="submitting" @click="submit">提交订单</el-button></el-form-item>
<el-card v-else shadow="never" class="form-card looseboard-form-card">
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
<div class="form-section transport-section">
<h3><span class="section-icon"><Truck :size="14" /></span>运输方式 <em>*</em></h3>
<div class="mode-buttons"><el-radio-group v-model="form.transportMode" @change="selectOriginMode"><el-radio-button value="SHARED_TRUCK"><Truck :size="14" /> 自送</el-radio-button><el-radio-button value="PRIORITY_CONSOLIDATION"><Package :size="14" /> 我司提货</el-radio-button></el-radio-group></div>
</div>
<div class="form-grid two-col">
<el-form-item :label="isSelfDelivery ? '入仓库' : '取货地'" prop="origin" :rules="[{ required: true, message: isSelfDelivery ? '请选择美国仓库' : '请输入取货地', trigger: isSelfDelivery ? 'change' : 'blur' }]">
<template v-if="isSelfDelivery">
<el-select v-model="form.origin" filterable clearable :loading="overseasWarehousesLoading" placeholder="请选择美国仓库" no-data-text="暂无可用的美国海外仓">
<el-option v-for="warehouse in overseasWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouseLabel(warehouse)" />
</el-select>
<div v-if="overseasWarehousesError" role="alert" class="warehouse-load-error">{{ overseasWarehousesError }} <el-button link type="primary" @click="loadOverseasWarehouses">重新加载</el-button></div>
</template>
<el-input v-else v-model.trim="form.origin" maxlength="255" placeholder="城市 / 港口 / 州" />
</el-form-item>
<el-form-item label="目的地" prop="destination"><el-input v-model.trim="form.destination" maxlength="255" placeholder="城市 / 州" /></el-form-item>
<el-form-item label="联系人" prop="contactName"><el-input v-model.trim="form.contactName" maxlength="128" placeholder="联系人姓名" /></el-form-item>
<el-form-item label="联系方式" prop="contactInfo"><el-input v-model.trim="form.contactInfo" maxlength="128" placeholder="联系电话" /></el-form-item>
</div>
<div class="form-section cargo-section"><h3><span class="section-icon"><Package :size="14" /></span>托载</h3><div class="form-grid cargo-grid">
<el-form-item label="商品名称"><el-input v-model.trim="form.cargoName" placeholder="请输入商品名称" /></el-form-item>
<el-form-item label="托数"><el-input-number v-model="form.palletCount" :min="0" controls-position="right" /></el-form-item>
<el-form-item label="总重量 (kg)"><el-input-number v-model="form.totalWeightKg" :min="0" :precision="2" controls-position="right" /></el-form-item>
<el-form-item label="总体积 (m³)"><el-input-number v-model="form.totalVolumeCbm" :min="0" :precision="3" controls-position="right" /></el-form-item>
</div></div>
<div class="form-grid two-col lower-grid">
<el-form-item label="预算总价($)"><el-input-number v-model="form.budgetAmount" :min="0" :precision="2" controls-position="right" placeholder="0.00" /></el-form-item>
<div></div>
<el-form-item label="要求取货时间" prop="requestedDate"><el-date-picker v-model="form.requestedDate" type="date" value-format="YYYY-MM-DD" placeholder="年 / 月 / 日" /></el-form-item>
<el-form-item label="送货到达时间"><el-date-picker v-model="form.deliveryDate" type="date" value-format="YYYY-MM-DD" placeholder="年 / 月 / 日" /></el-form-item>
</div>
<div class="form-actions"><el-button @click="returnToList">取消</el-button><el-button v-if="canCreate" type="primary" :loading="submitting" @click="submit">提交订单</el-button></div>
</el-form>
</el-card>
</section>
@@ -415,6 +455,31 @@ onMounted(() => { if (props.detailPage) void loadDetail(); else if (!props.creat
.form-card :deep(.el-form) { max-width: 860px; }
.form-card :deep(.el-input), .form-card :deep(.el-textarea), .form-card :deep(.el-select) { width: 100%; }
.form-card .currency { width: 110px; margin-left: 12px; }
.warehouse-load-error { color: #b42318; font-size: 12px; }
.looseboard-form-card { padding: 0; border: 1px solid #e7ebf1; border-radius: 10px; background: #fff; box-shadow: 0 8px 22px rgb(31 45 61 / 4%); }
.looseboard-form-card :deep(.el-card__body) { padding: 0; }
.looseboard-form-card :deep(.el-form) { max-width: none; }
.looseboard-form-card :deep(.el-form-item) { margin-bottom: 16px; }
.looseboard-form-card :deep(.el-form-item__label) { height: auto; padding-bottom: 7px; color: #27364d; font-size: 13px; font-weight: 600; line-height: 1.2; }
.looseboard-form-card :deep(.el-input__wrapper), .looseboard-form-card :deep(.el-textarea__inner), .looseboard-form-card :deep(.el-select__wrapper), .looseboard-form-card :deep(.el-input-number) { min-height: 36px; border-radius: 8px; box-shadow: 0 0 0 1px #e1e6ed inset; }
.looseboard-form-card :deep(.el-input-number) { width: 100%; }
.form-section { padding: 25px 30px 5px; }
.form-section h3 { display: flex; align-items: center; gap: 7px; margin: 0 0 15px; color: #27364d; font-size: 14px; }
.form-section h3 em { color: #e05252; font-style: normal; }
.section-icon { display: inline-flex; width: 20px; height: 20px; align-items: center; justify-content: center; border-radius: 5px; background: #fff3e0; color: #dd8d2c; }
.mode-buttons :deep(.el-radio-group) { display: flex; gap: 10px; }
.mode-buttons :deep(.el-radio-button__inner) { display: inline-flex; align-items: center; gap: 6px; height: 36px; padding: 0 18px; border: 1px solid #dce3eb; border-radius: 8px !important; color: #4b5563; box-shadow: none; }
.mode-buttons :deep(.el-radio-button:first-child .el-radio-button__inner) { border-left: 1px solid #dce3eb; }
.mode-buttons :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) { border-color: #5965d8; background: #f0f2ff; color: #4f5bd5; box-shadow: none; }
.form-grid { display: grid; gap: 0 22px; padding: 0 30px; }
.two-col { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.cargo-section { margin-top: 4px; padding-top: 7px; background: #fafafa; border-top: 1px solid #f0f2f5; border-bottom: 1px solid #f0f2f5; }
.cargo-grid { grid-template-columns: 1.25fr 1.25fr 1.25fr 1.25fr; }
.lower-grid { padding-top: 20px; }
.lower-grid :deep(.el-date-editor) { width: 100%; }
.form-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 5px; padding: 20px 30px 25px; border-top: 1px solid #f0f2f5; }
.form-actions .el-button { min-width: 80px; height: 36px; border-radius: 7px; }
.form-actions .el-button--primary { background: linear-gradient(135deg, #6774df, #7553aa); border: 0; box-shadow: 0 5px 12px rgb(103 116 223 / 24%); }
@media (max-width: 767px) {
.business-orders { gap: 14px; }
@@ -31,6 +31,14 @@ function harness(kind = 'disposal', overrides = {}) {
return { ...state, props, calls, events, revoked }
}
test('仓储转运客户确认测量弹窗不展示箱子明细和托盘尺寸', () => {
assert.doesNotMatch(source, /<el-table :data="measurement\.cartons\?\.filter\(carton => carton\.status === 'COUNTED'\)/)
assert.doesNotMatch(source, /<el-table-column label="尺寸\(cm\)"/)
assert.doesNotMatch(source, /箱数、尺寸、重量/)
assert.match(source, /<el-table-column prop="grossWeightKg" label="毛重\(kg\)"/)
assert.match(source, /<el-table-column prop="volumeCbm" label="体积\(m³\)"/)
})
for (const kind of ['disposal', 'labeling', 'transfer']) {
test(`${kind}: opening loads photos without confirming; only reviewed evidence can be submitted`, async () => {
const state = harness(kind)
@@ -96,21 +96,15 @@ onBeforeUnmount(clear)
<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)">
<p>单号:{{ documentNo }}</p>
<p>{{ kind === 'transfer' ? '请核对 PDA 回传的箱数、尺寸、重量、打托数据及现场照片,确认无误后再提交。' : '请核对以下 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>
<el-alert v-if="error" :title="error" type="error" :closable="false" />
@@ -334,6 +334,9 @@ const relabelCandidatesLoading = ref(false)
const transferInventoryCandidates = ref<RelabelInventoryCandidate[]>([])
const selectedTransferInventory = ref('')
const inventoryTransfer = computed(() => props.service === 'transfer' && Boolean(selectedTransferInventory.value))
// 来源库存发往FBA时默认贴标,私人仓和外来货保留原有选择。
const transferRelabelRequired = computed(() => inventoryTransfer.value && transferDestinationType.value === 'FBA')
watch(transferRelabelRequired, (required) => { if (required) form.relabel = true }, { flush: 'sync' })
const transferSourceCartons = ref<TransferSourceCarton[]>([])
const transferSourceLoading = ref(false)
const transferSourceError = ref('')
@@ -348,6 +351,8 @@ async function loadTransferSourceCartons() {
if (selectedTransferInventory.value.startsWith('RETURN_ORDER:')) {
const returnNo = selectedTransferInventory.value.slice('RETURN_ORDER:'.length)
transferSourceCartons.value = transferInventoryCandidates.value
.filter(item => !['COMPLETED', 'PARTIALLY_COMPLETED'].includes(item.relabelStatus || '')
&& Number(item.relabelledQuantity || item.relabelledPackages || 0) === 0)
.filter(item => (item.sourceType === 'RETURN_CARTON' || item.sourceType === 'RETURN_PARCEL')
&& String(item.inboundOrderNo || '').trim() === returnNo && Number(item.cartonAvailablePackages || 0) > 0)
.filter(item => Boolean(item.cartonMark))
@@ -1174,6 +1179,7 @@ function printForecastCartons() {
}
async function submit(action: 'draft' | 'submit' = 'submit') {
if (saving.value) return
if (transferRelabelRequired.value) form.relabel = true
const primaryWarehouse = props.service === 'relabel' ? form.sourceWarehouse : form.warehouse
const relabelRows = labelingMode.value === 'carton' ? cartonDetailRows.value : skuRelabelRows.value
const invalidCargo = props.service === 'disposal'
@@ -1473,7 +1479,7 @@ async function loadTransferInventoryCandidates(warehouseId: string) {
relabelCandidatesLoading.value = true
try {
// 共用接口包含退货历史及占用中的箱子,转运只展示当前可整箱使用的来源。
transferInventoryCandidates.value = (await listRelabelInventoryCandidates(warehouseId)).filter(item =>
transferInventoryCandidates.value = (await listRelabelInventoryCandidates(warehouseId)).filter(isUnlabelledTransferCandidate).filter(item =>
item.sourceType === 'RETURN_CARTON' || item.sourceType === 'RETURN_PARCEL'
? Number(item.cartonAvailablePackages || 0) > 0
: Number(item.availablePackages || 0) > 0)
@@ -1542,6 +1548,12 @@ const transferInventoryOptions = computed(() => {
}
return [...grouped.values()]
})
/** 已完成或部分完成换标的退货实物箱不再作为原箱来源;普通来源由逐箱接口过滤。 */
function isUnlabelledTransferCandidate(item: RelabelInventoryCandidate) {
if (item.sourceType !== 'RETURN_CARTON' && item.sourceType !== 'RETURN_PARCEL') return true
return !['COMPLETED', 'PARTIALLY_COMPLETED'].includes(item.relabelStatus || '')
&& inventoryQuantity(item.relabelledQuantity || item.relabelledPackages) === 0
}
async function selectTransferInventory(value: string) {
selectedTransferInventory.value = value || ''
selectedTransferSourceOrderIds.value = []
@@ -1730,7 +1742,7 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
<label v-if="props.service === 'labeling'"><span>贴标要求</span><el-input v-model="form.requirement" placeholder="请输入贴标要求" /></label>
<label v-else-if="props.service === 'relabel' || (props.service === 'transfer' && form.deliveryMethod === 'CUSTOMER_DELIVERY')"><span>物流跟踪号</span><el-input v-model="form.trackingNo" placeholder="请输入物流跟踪号(选填)" /></label>
<label class="warehouse-span-2"><span>备注</span><el-input v-model="form.remark" type="textarea" :rows="2" placeholder="选填" /></label>
<div v-if="props.service === 'transfer'" class="warehouse-choice-field warehouse-choice-field--relabel warehouse-span-2"><span><i>*</i>是否贴标</span><el-radio-group v-model="form.relabel"><el-radio-button :value="false">不需要贴标</el-radio-button><el-radio-button :value="true">需要贴标</el-radio-button></el-radio-group><small>{{ form.relabel ? '已开启贴标,请在右侧填写贴标资料' : 'FBA 仓和私人仓均可选择是否贴标' }}</small></div>
<div v-if="props.service === 'transfer'" class="warehouse-choice-field warehouse-choice-field--relabel warehouse-span-2"><span><i>*</i>是否贴标</span><el-radio-group v-model="form.relabel"><el-radio-button v-if="!transferRelabelRequired" :value="false">不需要贴标</el-radio-button><el-radio-button :value="true">需要贴标</el-radio-button></el-radio-group><small>{{ form.relabel ? '已开启贴标,请在右侧填写贴标资料' : 'FBA 仓和私人仓均可选择是否贴标' }}</small></div>
</div>
</div>
<div class="warehouse-card warehouse-form-section warehouse-transfer-cargo-card">
@@ -1805,7 +1817,7 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
<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>
<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>
@@ -2,6 +2,7 @@ import test from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { transpileModule, ScriptTarget } from 'typescript'
import { computed, ref, reactive, watch } from 'vue'
const view = readFileSync(new URL('./WarehouseServiceView.vue', import.meta.url), 'utf8')
@@ -18,6 +19,8 @@ test('退货箱明细与来源下拉使用同一份逐箱库存,不请求普
cartonSkuItems: [{ skuCode: 'A', quantity: 1 }] },
{ sourceType: 'RETURN_CARTON', inboundOrderNo: 'OTHER', cartonMark: 'FOREIGN', cartonAvailablePackages: 1 },
{ sourceType: 'RETURN_CARTON', inboundOrderNo: 'RET-1', cartonMark: 'OCCUPIED', cartonAvailablePackages: 0 },
{ sourceType: 'RETURN_CARTON', inboundOrderNo: 'RET-1', cartonMark: 'LABELLED', cartonAvailablePackages: 1, relabelStatus: 'COMPLETED' },
{ sourceType: 'RETURN_CARTON', inboundOrderNo: 'RET-1', cartonMark: 'PARTIAL', cartonAvailablePackages: 1, relabelledQuantity: 2 },
] }, getTransferSourceCartons: () => { throw Error('不应调用普通来源箱查询') } }
const load = new Function(...Object.keys(ctx), 'let transferSourceRequest=0;' + code + ';return loadTransferSourceCartons')(...Object.values(ctx))
await load()
@@ -28,6 +31,27 @@ test('退货箱明细与来源下拉使用同一份逐箱库存,不请求普
assert.equal(ctx.transferSourceLoading.value, false)
})
test('来源库存发往FBA默认开启贴标,清除来源或切换私人仓后恢复选项', () => {
const source = ref(''), destination = ref('NONE'), form = reactive({ relabel: false })
const inventoryTransfer = computed(() => Boolean(source.value))
const body = view.slice(view.indexOf('const transferRelabelRequired ='), view.indexOf('const transferSourceCartons ='))
const required = new Function('computed', 'watch', 'inventoryTransfer', 'transferDestinationType', 'form', body + ';return transferRelabelRequired')(
computed, watch, inventoryTransfer, destination, form)
destination.value = 'FBA'
assert.equal(form.relabel, false)
source.value = 'IN-1'
assert.equal(required.value, true)
assert.equal(form.relabel, true)
destination.value = 'PRIVATE'
assert.equal(required.value, false)
form.relabel = false
destination.value = 'FBA'
assert.equal(form.relabel, true)
source.value = ''
assert.equal(required.value, false)
assert.match(view, /<el-radio-button v-if="!transferRelabelRequired" :value="false">不需要贴标/)
})
function setup() {
const body = view.slice(view.indexOf('async function selectTransferInventory('), view.indexOf('function selectTransferInboundMethod('))
const code = transpileModule(body, { compilerOptions: { target: ScriptTarget.ES2022 } }).outputText