This commit is contained in:
2026-10-20 14:52:22 +08:00
parent 64697276ec
commit 9b1dec75dd
18 changed files with 78 additions and 175 deletions
@@ -285,7 +285,7 @@ onMounted(() => { if (props.detailPage) void Promise.all([loadCartonDetail(), lo
<template #time="{ row }"><span class="inventory-time">{{ formatInventoryTime(row.updatedAt || row.createdAt || row.receivedAt) }}</span></template>
</DataTable>
</div>
<el-dialog v-model="showPackingCreate" title="创建合箱计划" width="720px" destroy-on-close><el-form label-width="110px"><el-form-item label="已完成退货单"><el-input v-model="packingForm.returnNos" type="textarea" :rows="2" placeholder="输入退货单号,多个用空格或逗号分隔" /></el-form-item><el-form-item label="合箱 SKU"><div class="packing-sku-grid"><div v-for="item in summaryRows" :key="item.skuCode" class="packing-sku-row"><el-checkbox :model-value="packingForm.selectedSkus[item.skuCode] !== undefined" @change="togglePackingSku(item.skuCode, Boolean($event))">{{ item.skuCode }}</el-checkbox><span>可用 {{ item.availableQuantity }}</span><el-input-number v-if="packingForm.selectedSkus[item.skuCode] !== undefined" v-model="packingForm.selectedSkus[item.skuCode]" :min="1" :max="item.availableQuantity" :controls="false" placeholder="数量" /></div></div></el-form-item><el-form-item label="箱规"><div class="packing-dimensions"><el-input-number v-model="packingForm.length" :min="1" placeholder="长 cm" /><el-input-number v-model="packingForm.width" :min="1" placeholder="宽 cm" /><el-input-number v-model="packingForm.height" :min="1" placeholder="高 cm" /><el-input-number v-model="packingForm.weight" :min="1" placeholder="重量 kg" /></div></el-form-item><el-form-item label="备注"><el-input v-model="packingForm.remark" placeholder="可填写合箱说明" /></el-form-item></el-form><template #footer><el-button @click="showPackingCreate = false">取消</el-button><el-button type="primary" :loading="packingSaving" @click="submitPackingPlan">提交审核</el-button></template></el-dialog>
<el-dialog v-model="showPackingCreate" title="创建合箱计划" width="720px" destroy-on-close><el-form label-width="110px"><el-form-item label="已完成退货单"><el-input v-model="packingForm.returnNos" type="textarea" :rows="2" placeholder="输入退货单号,多个用空格或逗号分隔" /></el-form-item><el-form-item label="合箱 SKU"><div class="packing-sku-grid"><div v-for="item in summaryRows" :key="item.skuCode" class="packing-sku-row"><el-checkbox :model-value="packingForm.selectedSkus[item.skuCode] !== undefined" @change="togglePackingSku(item.skuCode, Boolean($event))">{{ item.skuCode }}</el-checkbox><span>可用 {{ item.availableQuantity }}</span><el-input-number v-if="packingForm.selectedSkus[item.skuCode] !== undefined" v-model="packingForm.selectedSkus[item.skuCode]" :min="1" :max="item.availableQuantity" :controls="false" placeholder="数量" /></div></div></el-form-item><el-form-item label="箱规"><div class="packing-dimensions"><el-input-number v-model="packingForm.length" :min="1" placeholder="长 cm" /><el-input-number v-model="packingForm.width" :min="1" placeholder="宽 cm" /><el-input-number v-model="packingForm.height" :min="1" placeholder="高 cm" /><el-input-number v-model="packingForm.weight" :min="1" placeholder="重量 kg" /></div></el-form-item><el-form-item label="备注"><el-input v-model="packingForm.remark" placeholder="" /></el-form-item></el-form><template #footer><el-button @click="showPackingCreate = false">取消</el-button><el-button type="primary" :loading="packingSaving" @click="submitPackingPlan">提交审核</el-button></template></el-dialog>
</template>
<article v-else v-loading="loading" class="carton-detail-surface">
<template v-if="cartonDetail">
@@ -7,39 +7,39 @@ const view = readFileSync(new URL('./ReturnOrdersView.vue', import.meta.url), 'u
const api = readFileSync(new URL('../api/returnOrders.ts', import.meta.url), 'utf8')
function listHarness(list, summary) {
const state = { query: { page: 1, size: 1, skuCode: 'FILTER' }, loading: { value: false }, listError: { value: '' }, statistics: { value: undefined }, rows: { value: [] }, total: { value: 0 } }
const state = { query: { page: 1, size: 1, skuCode: 'FILTER' }, loading: { value: false }, listError: { value: '' }, rows: { value: [] }, total: { value: 0 } }
const source = view.slice(view.indexOf('async function load()'), view.indexOf('\nfunction changePage('))
const js = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText
const load = new Function(...Object.keys(state), 'listReturnOrders', 'getReturnOrderStatistics', `let listRequest=0; ${js}; return load`)(...Object.values(state), list, summary)
return { ...state, load }
}
test('全筛选统计使用服务器总计,分页行只按单号映射载体,不重算SKU', async () => {
test('分页行按服务器单号映射载体,不展示全筛选汇总', async () => {
const calls = []
const totals = { orderCount: 3, skuTypes: 3, expectedQuantity: 19, receivedQuantity: 14, remainingQuantity: 5 }
const h = listHarness(async p => { calls.push(p); return { total: '3', records: [{ returnNo: 'RT-1', receivedQuantity: 8 }] } }, async p => { calls.push(p); return { totals, pageDimensions: [{ returnNo: 'RT-1', receivedParcelCount: 1, receivedBoxCount: 1 }] } })
await h.load()
assert.deepEqual(h.statistics.value, totals)
assert.equal(h.total.value, 3)
assert.doesNotMatch(view, /return-filter-summary|预报箱\/包裹\/托数/)
assert.equal(h.rows.value[0].receivedParcelCount, 1)
h.query.page = 2
await h.load()
assert.equal(h.statistics.value.receivedQuantity, 14)
assert.equal(h.rows.value[0].receivedBoxCount, 1)
assert.deepEqual(calls[2], calls[3])
assert.equal(calls[3].page, 2)
})
test('统计失败清空旧列表和汇总,并忽略较旧查询的迟到结果', async () => {
test('维度查询失败清空旧列表,并忽略较旧查询的迟到结果', async () => {
let fail = false, finish
const h = listHarness(async () => ({ total: 1, records: [{ returnNo: 'RT-1' }] }), async p => {
if (fail) throw Error('统计服务不可用')
if (p.page === 1) return new Promise(resolve => { finish = resolve })
return { totals: { orderCount: 2 }, pageDimensions: [] }
return { totals: { orderCount: 2 }, pageDimensions: [{ returnNo: 'RT-1', receivedBoxCount: 2 }] }
})
const old = h.load(); h.query.page = 2; await h.load()
finish({ totals: { orderCount: 999 }, pageDimensions: [] }); await old
assert.equal(h.statistics.value.orderCount, 2)
assert.equal(h.rows.value[0].receivedBoxCount, 2)
fail = true; await h.load()
assert.equal(h.statistics.value, undefined)
assert.deepEqual(h.rows.value, [])
assert.equal(h.listError.value, '统计服务不可用')
assert.equal(h.loading.value, false)
@@ -11,7 +11,7 @@ import { getMerchantWarehouses, type MerchantWarehouse } from '@/modules/first-l
import { createIdempotencyKey } from '@/utils/idempotencyKey'
import { createReturnOrder, downloadReturnPhoto, getReturnOrder, listReturnOrders, updateReturnOrder, type ReturnOrderDetail, type ReturnOrderSummary, type ReturnReceipt } from '../api/returnOrders'
import ReviewRejectionNotice from '../components/ReviewRejectionNotice.vue'
import { getReturnOrderStatistics, type ReturnOrderStatistics } from '../api/returnOrders'
import { getReturnOrderStatistics } from '../api/returnOrders'
import { getReturnInventoryLabel } from '../api/returnInventory'
import { formatReturnVolume, receiptBoxDetails, receiptPhysicalDetails, receiptPrintableBoxCount, receiptTrackingNo } from '../utils/returnReceiptDisplay'
@@ -41,7 +41,6 @@ const printableBoxCount = computed(() => receiptPrintableBoxCount(printableRecei
const photoUrls = reactive<Record<string, string>>({})
const rows = ref<ReturnOrderSummary[]>([])
const total = ref(0)
const statistics = ref<ReturnOrderStatistics['totals']>()
const listError = ref('')
let listRequest = 0
const operationWarehouses = ref<MerchantWarehouse[]>([])
@@ -158,18 +157,18 @@ function removeItem(index: number) { if (form.items.length > 1) form.items.splic
async function load() {
const request = ++listRequest
loading.value = true
listError.value = ''; statistics.value = undefined
listError.value = ''
try {
const params = { ...query, returnNo: query.returnNo || undefined, trackingNo: query.trackingNo || undefined, skuCode: query.skuCode || undefined, status: query.status || undefined }
const [response, summary] = await Promise.all([listReturnOrders(params), getReturnOrderStatistics(params)])
if (request !== listRequest) return
const dimensions = new Map(summary.pageDimensions.map(row => [row.returnNo, row]))
rows.value = response.records.map(row => ({ ...row, ...dimensions.get(row.returnNo) }))
total.value = Number(response.total); statistics.value = summary.totals
total.value = Number(response.total)
} catch (error) {
if (request !== listRequest) return
rows.value = []; total.value = 0
listError.value = error instanceof Error ? error.message : '退货列表及汇总加载失败'
listError.value = error instanceof Error ? error.message : '退货列表加载失败'
} finally { if (request === listRequest) loading.value = false }
}
function changePage(page: number) {
@@ -345,10 +344,6 @@ onBeforeUnmount(clearPhotoUrls)
<template v-else-if="!props.detailPage">
<QueryToolbar v-model="queryModel" class="return-query" layout="grid" ariaLabel="退货签收筛选" :fields="queryFields" @search="search" />
<el-alert v-if="listError" type="error" :closable="false" :title="listError"><el-button @click="load">重新加载</el-button></el-alert>
<div v-if="statistics && !loading" class="return-filter-summary" role="status" aria-label="退货全筛选汇总">
全筛选:{{ statistics.orderCount }} 票 · {{ statistics.skuTypes }} 种 SKU · 预报 {{ statistics.expectedQuantity }} 件 · 实收 {{ statistics.receivedQuantity }} 件 · 待收 {{ statistics.remainingQuantity }} 件 · 签收 {{ statistics.receivedBoxCount }} 箱 / {{ statistics.receivedParcelCount }} 包裹 / {{ statistics.receivedPalletCount }} 托
<span>预报箱/包裹/托数:未提供</span>
</div>
<div class="return-table-card">
<DataTable v-model:page="query.page" v-model:page-size="query.size" :data="rows" :columns="tableColumns" :loading="loading" :total="total" :page-sizes="[15, 30, 50, 100]" :row-actions="rowActions" row-key="returnNo" empty-text="暂无退货预报">
<template #toolbar-actions><el-button v-if="canCreate" type="primary" @click="openCreate"><Plus :size="16" />新建退货预报</el-button></template>
@@ -501,8 +496,6 @@ onBeforeUnmount(clearPhotoUrls)
</template>
<style scoped>
.return-filter-summary { padding:12px 16px; margin-bottom:12px; border:1px solid var(--client-border); border-radius:8px; background:var(--client-surface,#fff); line-height:1.8; }
.return-filter-summary span { margin-left:16px; color:var(--client-text-secondary,#64748b); }
.return-query.query-toolbar--grid { display:grid; grid-template-columns:repeat(4,minmax(0,280px)) max-content; gap:14px 18px; align-items:end; }
.return-query :deep(.query-toolbar__filters) { display:contents; }
.return-query :deep(.query-toolbar__field) { display:flex; flex-direction:column; align-items:stretch; gap:6px; }
@@ -1,9 +1,7 @@
<script setup lang="ts">
import RelabelQuantityPanel from '../components/RelabelQuantityPanel.vue'
import TransferQuantityPanel from '../components/TransferQuantityPanel.vue'
import { getRelabelQuantities, type RelabelQuantity } from '../api/relabelQuantities'
import TransferPickupProgressPanel from '../components/TransferPickupProgressPanel.vue'
import DiscardStatistics from '../components/DiscardStatistics.vue'
import TransferPaneDivider from '../components/TransferPaneDivider.vue'
import { computed, nextTick, onActivated, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
@@ -137,13 +135,9 @@ const skuRelabelRows = ref<SkuRelabelRow[]>([])
const importedRelabelBindings = ref<ImportedRelabelBinding[]>([])
const cartonDetailRows = computed(() => cargoRows.value.filter((row) => row.included))
const submittedRows = ref<Record<string, string>[]>([])
const discardStatisticsQuery = ref<Record<string, unknown>>({ page: 1, size: 15 })
const discardStatisticsRevision = ref(0)
const page = ref(1)
const pageSize = ref(15)
const total = ref(0)
const transferQuantityQuery = ref<Record<string, unknown>>({ page: 1, size: 15 })
const relabelQuantityQuery = ref<Record<string, unknown>>({ page: 1, size: 15 })
const editingTransferNo = ref('')
interface TransferPallet { id?: string | number; sequenceNo?: number; palletNo?: string; warehouseLocationCode?: string; cartonCount?: number; lengthCm?: number; widthCm?: number; heightCm?: number; grossWeightKg?: number; volumeCbm?: number; status?: string; completedAt?: string }
interface TransferMeasurementPhoto { fileId: string | number; fileName?: string; contentType?: string; sizeBytes?: number; uploadedAt?: string }
@@ -1138,7 +1132,6 @@ async function load() {
try {
if (props.service === 'labeling') {
const query = { page: page.value, size: pageSize.value, relabelNo: filters.number || undefined, overseasWarehouseId: filters.warehouse || undefined, status: filters.status || undefined, createdFrom: dateTime(filters.start), createdTo: dateTime(filters.end, true), reviewedFrom: dateTime(filters.reviewStart), reviewedTo: dateTime(filters.reviewEnd, true), completedFrom: dateTime(filters.completedStart), completedTo: dateTime(filters.completedEnd, true), outboundFrom: dateTime(filters.outboundStart), outboundTo: dateTime(filters.outboundEnd, true) }
relabelQuantityQuery.value = query
const [result, dimensionResult] = await Promise.all([listRelabels(query), getRelabelQuantities(query).catch(() => null)])
total.value = Number.isFinite(Number(result.total)) ? Number(result.total) : result.records.length
submittedRows.value = result.records.map((item) => {
@@ -1147,14 +1140,12 @@ async function load() {
})
} else if (props.service === 'disposal') {
const discardQuery = { page: page.value, size: pageSize.value, discardNo: filters.number || undefined, inboundCode: filters.inboundNo || undefined, overseasWarehouseId: filters.warehouse || undefined, status: filters.status || undefined, createdFrom: dateTime(filters.start), createdTo: dateTime(filters.end, true), completedFrom: dateTime(filters.completedStart), completedTo: dateTime(filters.completedEnd, true) }
discardStatisticsQuery.value = discardQuery; discardStatisticsRevision.value++
const result = await listDiscards(discardQuery)
if (discardRequest !== discardLoadGeneration || props.service !== 'disposal') return
total.value = Number.isFinite(Number(result.total)) ? Number(result.total) : result.records.length
submittedRows.value = result.records.map((item) => ({ ...row([item.discardNo, item.outboundNo || '—', item.inboundCodes, item.overseasWarehouseName || warehouseDisplay(item.overseasWarehouseId) || '—', item.skuCodes?.length ? `${item.skuCount} 种|${item.skuCodes.join('、')}` : '未记录', item.requestedPackages, `${item.currency} ${item.totalFee}`, statusLabel(item.status), formatDateTime(item.customerConfirmedAt), formatDateTime(item.createdAt)]), __status: item.status }))
} else {
const query = { page: page.value, size: pageSize.value, transferNo: filters.number || undefined, inboundCode: filters.inboundNo || undefined, operatingWarehouseId: filters.warehouse || undefined, fbaWarehouseCode: filters.destination || undefined, status: filters.status || undefined, ...(props.service === 'relabel' ? { needRelabel: true } : {}), createdFrom: dateTime(filters.start), createdTo: dateTime(filters.end, true), reviewedFrom: dateTime(filters.reviewStart), reviewedTo: dateTime(filters.reviewEnd, true), measuredFrom: props.service === 'relabel' ? dateTime(filters.measuredStart) : dateTime(filters.completedStart), measuredTo: props.service === 'relabel' ? dateTime(filters.measuredEnd, true) : dateTime(filters.completedEnd, true), outboundFrom: dateTime(filters.outboundStart), outboundTo: dateTime(filters.outboundEnd, true) }
transferQuantityQuery.value = query
const result = await listTransfers(query)
total.value = Number.isFinite(Number(result.total)) ? Number(result.total) : result.records.length
submittedRows.value = result.records.map((item) => {
@@ -1715,10 +1706,7 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
<template>
<section class="warehouse-service-page" :class="{ 'warehouse-service-page--detail': mode === 'detail', 'warehouse-service-page--transfer': props.service === 'transfer', 'warehouse-service-page--disposal-form': props.service === 'disposal' && (mode === 'create' || mode === 'edit'), 'warehouse-service-page--return-relabel': isReturnRelabelSource }">
<template v-if="mode === 'list'">
<TransferQuantityPanel v-if="props.service === 'transfer'" :query="transferQuantityQuery" />
<RelabelQuantityPanel v-if="props.service === 'labeling'" :query="relabelQuantityQuery" />
<QueryToolbar v-model="listQueryModel" :class="{ 'labeling-query': props.service === 'labeling', 'transfer-query': props.service === 'transfer' || props.service === 'disposal', 'disposal-query': props.service === 'disposal' }" layout="grid" :ariaLabel="`${config.title}筛选`" :fields="listQueryFields" :search-on-reset="false" @search="query" @reset="resetListQuery" />
<DiscardStatistics v-if="props.service === 'disposal'" :query="discardStatisticsQuery" :revision="discardStatisticsRevision" />
<div class="warehouse-card warehouse-table-card">
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="submittedRows" :columns="listColumns" :loading="loading" :total="total" :page-sizes="[15, 30, 50, 100]" :row-actions="listRowActions" :empty-text="config.emptyText">
<template #toolbar-actions><el-button v-if="canCreate" type="primary" @click="openCreate"><Plus :size="16" />{{ props.service === 'relabel' ? '新建退货换标单' : props.service === 'transfer' ? '新增订单' : `新增${config.title}` }}</el-button></template>
@@ -1746,8 +1734,8 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
<div v-show="props.service !== 'labeling' || !orderInfoCollapsed" class="warehouse-form-grid">
<label v-if="props.service === 'disposal'" class="warehouse-disposal-field"><span><i>*</i>操作仓</span><el-select v-model="form.warehouse" filterable clearable :loading="fbaWarehousesLoading" :disabled="Boolean(operationWarehousesError)" placeholder="" @change="selectDiscardWarehouse"><el-option v-for="warehouse in operationWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="String(warehouse.id)" /></el-select><small v-if="operationWarehousesError" class="warehouse-hint">{{ operationWarehousesError }}</small></label>
<label v-if="props.service === 'disposal'" class="warehouse-disposal-field"><span><i>*</i>选择入库单</span><el-select v-model="selectedDiscardOrderId" filterable clearable :loading="discardCandidatesLoading" :disabled="!form.warehouse" no-data-text="当前海外仓暂无可废弃库存" placeholder="" @change="selectDiscardInbound"><el-option v-for="candidate in discardDocuments" :key="candidate.orderId" :label="`${candidate.sourceType === 'RETURN_CARTON' ? '退货|' : ''}${candidate.inboundOrderNo || candidate.inboundCode || '—'}|可用 ${candidate.availablePackages} 箱`" :value="candidate.orderId" /></el-select></label>
<label v-if="props.service === 'relabel'"><span><i>*</i>进仓编号</span><el-input v-model="form.inboundNo" placeholder="请输入已有入仓编号" /></label>
<label v-if="props.service === 'relabel'"><span><i>*</i>来源 FBA 仓库</span><el-select v-model="form.sourceWarehouse" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" placeholder="请选择平台维护的 FBA 仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select></label>
<label v-if="props.service === 'relabel'"><span><i>*</i>进仓编号</span><el-input v-model="form.inboundNo" placeholder="" /></label>
<label v-if="props.service === 'relabel'"><span><i>*</i>来源 FBA 仓库</span><el-select v-model="form.sourceWarehouse" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" placeholder=""><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select></label>
<label v-else-if="props.service === 'disposal'" class="warehouse-disposal-field"><span><i>*</i>废弃原因</span><el-select v-model="form.reason" placeholder=""><el-option label="残损不可售" value="残损不可售" /><el-option label="其他" value="其他" /></el-select></label>
<label v-else><span><i>*</i>{{ props.service === 'labeling' ? '海外操作仓(中转仓)' : props.service === 'transfer' ? '入库海外仓' : '操作仓' }}</span><el-select v-model="form.warehouse" :loading="fbaWarehousesLoading || operationWarehouseDetailLoading" :disabled="Boolean(operationWarehousesError)" clearable placeholder="" @change="props.service === 'labeling' ? selectOperationWarehouse() : props.service === 'transfer' && selectTransferWarehouse()"><el-option v-for="warehouse in operationWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="String(warehouse.id)" /></el-select><small v-if="operationWarehousesError" class="warehouse-hint">{{ operationWarehousesError }}</small></label>
<label v-if="props.service === 'labeling'"><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></label>
@@ -1763,17 +1751,17 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
<div><span>收货联系人</span><strong>{{ selectedOperationWarehouse?.warehouse.contactName || '未维护' }}</strong></div>
<div><span>联系电话</span><strong>{{ selectedOperationWarehouse?.warehouse.contactPhone || '未维护' }}</strong></div>
</div>
<label v-if="props.service === 'relabel'"><span><i>*</i>目的 FBA 仓</span><el-select v-model="form.destination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" placeholder="请选择平台维护的 FBA 仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select></label>
<label v-if="props.service === 'relabel'"><span><i>*</i>派送方式</span><el-select v-model="form.deliveryMethod" placeholder="请选择"><el-option label="卡车派送" value="卡车派送" /><el-option label="快递派送" value="快递派送" /></el-select></label>
<label v-if="props.service === 'relabel'"><span><i>*</i>退货原因</span><el-select v-model="form.reason" placeholder="请选择"><el-option label="客户退货" value="客户退货" /><el-option label="亚马逊移除" value="亚马逊移除" /><el-option label="残损不可售" value="残损不可售" /></el-select></label>
<label v-if="props.service === 'relabel'"><span><i>*</i>目的 FBA 仓</span><el-select v-model="form.destination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" placeholder=""><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select></label>
<label v-if="props.service === 'relabel'"><span><i>*</i>派送方式</span><el-select v-model="form.deliveryMethod" placeholder=""><el-option label="卡车派送" value="卡车派送" /><el-option label="快递派送" value="快递派送" /></el-select></label>
<label v-if="props.service === 'relabel'"><span><i>*</i>退货原因</span><el-select v-model="form.reason" placeholder=""><el-option label="客户退货" value="客户退货" /><el-option label="亚马逊移除" value="亚马逊移除" /><el-option label="残损不可售" value="残损不可售" /></el-select></label>
<label v-if="props.service === 'labeling'"><span><i>*</i>海外仓签收联系人<em v-if="form.warehouse && !form.contact" class="warehouse-unmaintained">未维护</em></span><el-input v-model="form.contact" placeholder="" /></label>
<label v-if="props.service === 'labeling'"><span><i>*</i>海外仓联系电话<em v-if="form.warehouse && !form.phone" class="warehouse-unmaintained">未维护</em></span><el-input v-model="form.phone" placeholder="" /></label>
<label v-if="props.service === 'labeling'"><span><i>*</i>贴标截止时间</span><el-date-picker v-model="form.labelDeadline" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="" /></label>
<label v-if="props.service === 'labeling' && labelingMode !== 'sku'"><span><i>*</i>换标后派送方式</span><el-select v-model="form.deliveryMethod" placeholder=""><el-option label="卡车派送" value="TRUCK" /><el-option label="快递派送" value="EXPRESS" /><el-option label="亚马逊自提" value="AMAZON_PICKUP" /><el-option label="客户自提" value="CUSTOMER_PICKUP" /></el-select></label>
<div v-if="props.service === 'labeling' && labelingMode !== 'sku' && form.deliveryMethod === 'EXPRESS'" class="warehouse-checkbox-field"><span>是否打印面单</span><el-checkbox v-model="form.relabel">需要仓库打印面单</el-checkbox></div>
<label v-if="canManageRelabelFiles && props.service === 'labeling' && labelingMode !== 'sku' && form.deliveryMethod === 'EXPRESS' && form.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'"><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'" 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 === '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>
<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>
@@ -1783,10 +1771,10 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
<label v-if="props.service === 'transfer' && transferDestinationType === 'PRIVATE'"><span><i>*</i>邮编</span><el-input v-model="form.privateDestinationPostalCode" maxlength="32" placeholder="" /></label>
<label v-if="props.service === 'transfer' && transferDestinationType === 'PRIVATE'"><span><i>*</i>私人仓联系人</span><el-input v-model="form.privateDestinationContact" maxlength="128" placeholder="" /></label>
<label v-if="props.service === 'transfer' && transferDestinationType === 'PRIVATE'"><span><i>*</i>联系电话</span><el-input v-model="form.privateDestinationPhone" maxlength="64" placeholder="" /></label>
<label v-if="props.service === 'relabel'"><span><i>*</i>FBA 货件编号</span><el-input v-model="form.fbaShipmentNo" placeholder="请输入 FBA 货件编号" /></label>
<label v-if="props.service === 'relabel'"><span><i>*</i>FBA 货件编号</span><el-input v-model="form.fbaShipmentNo" placeholder="" /></label>
<label v-if="props.service === 'labeling'"><span>贴标要求</span><el-input v-model="form.requirement" placeholder="" /></label>
<label v-else-if="props.service === 'relabel' || (props.service === 'transfer' && form.deliveryMethod === 'CUSTOMER_DELIVERY')"><span>物流跟踪号</span><el-input v-model="form.trackingNo" :placeholder="props.service === 'transfer' ? '' : '请输入物流跟踪号(选填)'" /></label>
<label class="warehouse-span-2"><span>备注</span><el-input v-model="form.remark" type="textarea" :rows="2" :placeholder="props.service === 'relabel' ? '选填' : ''" /></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 v-if="!transferRelabelRequired" :value="false">不需要贴标</el-radio-button><el-radio-button :value="true">需要贴标</el-radio-button></el-radio-group></div>
</div>
</div>
@@ -1915,7 +1903,7 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
<div class="warehouse-section-heading warehouse-transfer-receipt-heading"><h3>预报箱唛({{ selectedDetail?.packageCount || 0 }} 箱)</h3><div class="warehouse-transfer-receipt-actions"><el-button v-if="canIncreaseForecast" type="primary" size="small" @click="openIncreaseForecast"><Plus :size="14" />增加预报箱数</el-button><el-button type="primary" size="small" @click="printTransferCartons(String(selectedDetail?.transferNo || ''))"><Printer :size="14" />打印预报箱唛</el-button><el-button size="small" @click="showForecastCartons = true">查看预报箱唛</el-button></div></div>
<el-dialog v-model="showForecastCartons" title="预报箱唛明细" width="900px" align-center destroy-on-close append-to-body><el-table :data="selectedDetail?.cartons || []" size="small" border><el-table-column prop="cartonMark" label="仓库唛头" min-width="300" /><el-table-column label="预报SKU" min-width="240"><template #default="{ row }">{{ selectedDetail?.items?.find(item => String(item.id) === String(row.transferItemId))?.skuCode || '—' }}</template></el-table-column><el-table-column label="清点状态" width="130"><template #default="{ row }">{{ row.status === 'COUNTED' ? '已清点' : row.status === 'NOT_RECEIVED' ? '未收到' : '待清点' }}</template></el-table-column></el-table><template #footer><el-button @click="showForecastCartons = false">关闭</el-button></template></el-dialog>
<el-dialog v-model="increaseForecastOpen" title="增加预报箱数" width="560px" align-center destroy-on-close append-to-body :close-on-click-modal="!increasingForecast" :show-close="!increasingForecast">
<el-form label-width="110px"><el-form-item label="新增箱数"><el-input-number v-model="increaseForecastForm.additionalCount" placeholder="请输入新增箱数" :disabled="increasingForecast" :min="1" :max="Math.max(1, 5000 - Number(forecastSnapshot?.packageCount || 0))" :precision="0" /></el-form-item></el-form><p class="warehouse-form-hint">保留原箱唛,仅补生成新增箱唛;箱内SKU和件数以PDA清点为准,SKU预报总件数保持不变。</p>
<el-form label-width="110px"><el-form-item label="新增箱数"><el-input-number v-model="increaseForecastForm.additionalCount" placeholder="" :disabled="increasingForecast" :min="1" :max="Math.max(1, 5000 - Number(forecastSnapshot?.packageCount || 0))" :precision="0" /></el-form-item></el-form><p class="warehouse-form-hint">保留原箱唛,仅补生成新增箱唛;箱内SKU和件数以PDA清点为准,SKU预报总件数保持不变。</p>
<template #footer><el-button :disabled="increasingForecast" @click="increaseForecastOpen = false">取消</el-button><el-button type="primary" :loading="increasingForecast" @click="saveIncreaseForecast">确认增加</el-button></template>
</el-dialog>
</div>
@@ -1980,7 +1968,7 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
<div v-if="props.service === 'transfer' && selectedDetail?.relabelNo" class="warehouse-card warehouse-form-section"><RelabelDocumentsPanel :relabel-no="selectedDetail.relabelNo" /></div>
<div v-if="props.service === 'labeling'" class="warehouse-card warehouse-form-section">
<div class="warehouse-section-heading"><h3>贴标明细资料</h3><input ref="relabelFileInput" class="warehouse-hidden-input" type="file" accept="application/pdf,.pdf" @change="uploadSelectedRelabelFile"></div>
<div class="warehouse-hint">需要补充或替换资料时,可在对应明细行上传 SKU 或箱唛文件。</div>
<div class="warehouse-table-scroll"><table class="warehouse-relabel-detail-table"><thead><tr><th>换标模式</th><th>仓库唛头</th><th>SKU</th><th>明细文件</th><th>操作</th></tr></thead><tbody>
<tr v-for="item in selectedRelabelDetail?.items || []" :key="item.id"><td>{{ item.operationType === 'CARTON_AND_SKU' ? '箱唛 + SKU 换标' : ['SKU_LABEL', 'RETURN_BULK_SKU_LABEL'].includes(item.operationType || '') ? 'SKU 换标' : '箱唛换标' }}</td><td>{{ item.cartonMark || '—' }}</td><td>{{ relabelSkuCodes(item).join('、') || '—' }}</td><td>{{ item.relabelFileName || '未上传' }}</td><td><el-button size="small" @click="chooseRelabelFile('ITEM', String(item.id))"><Upload :size="14" />上传明细文件</el-button></td></tr>
<tr v-if="!selectedRelabelDetail?.items?.length"><td colspan="5"><div class="warehouse-empty">暂无贴标明细</div></td></tr>