Files
client-web/src/modules/overseas-warehouse/views/WarehouseServiceView.vue
T

855 lines
99 KiB
Vue
Raw Normal View History

2026-08-21 17:20:51 +08:00
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
2026-09-13 21:10:14 +08:00
import { ArrowLeft, Download, Eye, Plus, Printer, Search, Trash2, Upload } from '@lucide/vue'
2026-09-07 19:15:18 +08:00
import SingleLineText from '@/components/common/SingleLineText.vue'
2026-08-21 17:20:51 +08:00
import {
createDiscard, createRelabel, createTransfer, getDiscard, getRelabel, getTransfer,
2026-09-13 21:10:14 +08:00
getTransferCartonLabel,
confirmRelabelCompletion, confirmTransferMeasurement, downloadRelabelCompletionPhoto, uploadRelabelShippingLabel,
listDiscardCandidates, listDiscards, listRelabelInventoryCandidates, listRelabels, listTransfers,
2026-09-08 20:28:05 +08:00
type RelabelInventoryCandidate,
type RelabelCreate,
2026-09-06 18:11:42 +08:00
type RelabelDetail,
2026-08-21 17:20:51 +08:00
} from '../api/warehouseServices'
2026-09-10 15:34:03 +08:00
import {
getMerchantWarehouseDetail,
getMerchantWarehouses,
type MerchantWarehouse,
type MerchantWarehouseDetail,
} from '@/modules/first-leg/api/firstLeg'
2026-08-21 17:20:51 +08:00
type ServiceType = 'labeling' | 'transfer' | 'disposal' | 'relabel'
type PageMode = 'list' | 'create' | 'detail'
2026-09-09 13:35:51 +08:00
type LabelingMode = 'carton' | 'sku' | 'carton_and_sku'
2026-09-10 17:12:52 +08:00
type RelabelTemplateType = 'carton' | 'sku' | 'mixed'
2026-09-13 21:10:14 +08:00
interface CargoRow { id: number; included?: boolean; sku: string; name: string; quantity: number; availableQuantity?: number; sourceOrderId?: string; sourceInventoryBalanceId?: string; sourceInventoryPositionId?: string; sourceInboundOrderId?: string; sourceInboundOrderNo?: string; sourceInboundCode?: string; sourceWarehouseName?: string; palletNo?: string; warehouseLocationCode?: string; fnsku: string; shipmentNo: string; newSkuCode: string; shippingMark: string; customerCartonMarks: string; cartonMark: string; fbaCartonMark: string; destinationWarehouseId: string; remark: string }
2026-09-09 13:35:51 +08:00
interface SkuRelabelRow { id: number; sourceRowId: number | null; cartonMark: string; originalSku: string; newSku: string; quantity: number; fbaCartonMark: string; destinationWarehouseId: string; remark: string }
2026-09-10 17:12:52 +08:00
interface ImportedRelabelBinding extends SkuRelabelRow { templateType: RelabelTemplateType; selected: boolean; shipmentNo: string }
2026-08-21 17:20:51 +08:00
interface ServiceConfig { title: string; createTitle: string; numberLabel: string; emptyText: string; columns: string[] }
2026-09-13 21:10:14 +08:00
interface TransferCarton { cartonMark: string; customerCartonMark?: string; cartonIndex: number; totalCartons: number; status: string }
2026-08-21 17:20:51 +08:00
const props = defineProps<{ service: ServiceType }>()
const configs: Record<ServiceType, ServiceConfig> = {
2026-09-08 20:28:05 +08:00
labeling: { title: '贴标申请', createTitle: '新增贴标申请', numberLabel: '贴标申请单号', emptyText: '暂无贴标申请', columns: ['操作', '贴标申请单号', '客户单号', '海外操作仓', '贴标要求', 'SKU 数', '总箱数', '状态', '创建时间'] },
transfer: { title: '仓储转运单', createTitle: '新增仓储转运单', numberLabel: '转运单号', emptyText: '暂无仓储转运订单', columns: ['操作', '转运单号', '入库箱数', '中转仓', 'FBA 目的仓', '入仓方式', '是否换标', '状态', '创建时间'] },
2026-09-08 20:28:05 +08:00
disposal: { title: '货物废弃', createTitle: '新增货物废弃申请', numberLabel: '废弃申请单号', emptyText: '暂无货物废弃申请', columns: ['操作', '废弃申请单号', '进仓编号', '操作仓', 'SKU 汇总', '废弃数量', '预估费用', '状态', '创建时间'] },
relabel: { title: '退货换标', createTitle: '新建退货换标单', numberLabel: '退货换标单号', emptyText: '暂无退货换标订单', columns: ['操作', '退货换标单号', '来源 FBA 仓', '目的 FBA 仓', 'SKU 汇总', '退件总数', '是否换标', '换标状态', '尾程状态', '创建时间'] },
2026-08-21 17:20:51 +08:00
}
const config = computed(() => configs[props.service])
const statusOptions = computed(() => props.service === 'labeling'
2026-09-10 15:34:03 +08:00
? [{ label: '待处理', value: 'PENDING_PROCESSING' }, { label: '处理中', value: 'PROCESSING' }, { label: '待客户确认', value: 'CUSTOMER_CONFIRM_PENDING' }, { label: '待出库', value: 'READY_FOR_OUTBOUND' }, { label: '已完成', value: 'COMPLETED' }, { label: '已拒绝', value: 'REJECTED' }, { label: '已取消', value: 'CANCELLED' }]
2026-08-21 17:20:51 +08:00
: props.service === 'disposal'
? [{ label: '待审核', value: 'PENDING_REVIEW' }, { label: '已通过', value: 'APPROVED' }, { label: '已拒绝', value: 'REJECTED' }]
: [{ label: '待审核', value: 'PENDING_REVIEW' }, { label: '已通过', value: 'APPROVED' }, { label: '待分配', value: 'PENDING_ASSIGNMENT' }, { label: '已分配', value: 'ALLOCATED' }, { label: '已出库', value: 'OUTBOUNDED' }, { label: '部分收货', value: 'PARTIALLY_RECEIVED' }, { label: '已收货', value: 'RECEIVED' }, { label: '已拒绝', value: 'REJECTED' }])
const mode = ref<PageMode>('list')
const filters = reactive({ number: '', inboundNo: '', warehouse: '', destination: '', status: '', start: '', end: '' })
2026-09-13 21:10:14 +08:00
const form = reactive({ customerNo: '', inboundNo: '', sourceWarehouse: '', warehouse: '', destination: '', address: '', contact: '', phone: '', deliveryMethod: '', relabelDeliveryMethod: '', trackingNo: '', reason: '', requirement: '', relabel: false, remark: '', labelDeadline: '', estimatedArrivalAt: '', estimatedShipAt: '', fbaShipmentNo: '', volumeCbm: 0.01, chargeAmount: 0 })
2026-09-10 15:34:03 +08:00
const transferHasFbaDestination = ref(false)
const selectedOperationWarehouse = ref<MerchantWarehouseDetail | null>(null)
const operationWarehouseDetailLoading = ref(false)
const operationWarehouseAddress = computed(() => {
const address = selectedOperationWarehouse.value?.addresses[0]
if (!address) return ''
return [address.addressLine1, address.addressLine2, address.city, address.stateProvince, address.postalCode, address.countryCode]
.filter(Boolean).join(', ')
})
2026-08-21 17:20:51 +08:00
const cargoRows = ref<CargoRow[]>([])
2026-09-13 21:10:14 +08:00
const transferRelabelRows = ref<CargoRow[]>([])
2026-09-08 20:28:05 +08:00
const labelingMode = ref<LabelingMode>('carton')
2026-09-10 15:34:03 +08:00
const shippingLabelFile = ref<File | null>(null)
2026-09-08 20:28:05 +08:00
const skuRelabelRows = ref<SkuRelabelRow[]>([])
2026-09-10 17:12:52 +08:00
const importedRelabelBindings = ref<ImportedRelabelBinding[]>([])
const cartonDetailRows = computed(() => cargoRows.value.filter((row) => row.included))
2026-09-09 13:35:51 +08:00
const labelingModeHint = computed(() => labelingMode.value === 'carton'
? '只替换外箱箱唛,不修改箱内商品 SKU 标签;按箱填写新的 FBA 箱唛和目的仓。'
: labelingMode.value === 'sku'
? '只替换箱内商品 SKU 标签,保留原箱唛;同一箱唛可以添加多条 SKU 明细。'
: '同一箱同时替换商品 SKU 标签和外箱箱唛;同一箱唛的多条 SKU 使用同一个新 FBA 箱唛和目的仓。')
2026-08-21 17:20:51 +08:00
const submittedRows = ref<Record<string, string>[]>([])
2026-09-13 21:10:14 +08:00
const selectedDetail = ref<Record<string, any> | null>(null)
2026-09-06 18:11:42 +08:00
const selectedRelabelDetail = ref<RelabelDetail | null>(null)
2026-09-08 20:28:05 +08:00
const fbaWarehouses = ref<MerchantWarehouse[]>([])
const operationWarehouses = ref<MerchantWarehouse[]>([])
const fbaWarehousesLoading = ref(false)
const fbaWarehousesError = ref('')
2026-09-13 21:10:14 +08:00
const operationWarehousesError = ref('')
2026-08-21 17:20:51 +08:00
const discardCandidates = ref<import('../api/warehouseServices').DiscardCandidate[]>([])
const selectedDiscardOrderId = ref('')
const discardCandidatesLoading = ref(false)
2026-09-08 20:28:05 +08:00
const relabelCandidates = ref<RelabelInventoryCandidate[]>([])
const selectedRelabelInboundOrderId = ref('')
const relabelCandidatesLoading = ref(false)
const relabelInboundOptions = computed(() => {
const grouped = new Map<string, { value: string; inboundOrderNo: string; inboundCode: string; availablePackages: number }>()
for (const candidate of relabelCandidates.value) {
const value = String(candidate.inboundOrderId)
const current = grouped.get(value)
if (current) current.availablePackages += candidate.availablePackages
else grouped.set(value, {
value,
inboundOrderNo: candidate.inboundOrderNo || '—',
inboundCode: candidate.inboundCode || '—',
availablePackages: candidate.availablePackages,
})
}
return [...grouped.values()]
})
2026-08-21 17:20:51 +08:00
const loading = ref(false)
const saving = ref(false)
2026-09-10 17:12:52 +08:00
const cartonTemplateInput = ref<HTMLInputElement | null>(null)
const skuTemplateInput = ref<HTMLInputElement | null>(null)
const mixedTemplateInput = ref<HTMLInputElement | null>(null)
2026-09-13 21:10:14 +08:00
const transferCartonMarkInput = ref<HTMLInputElement | null>(null)
2026-08-21 17:20:51 +08:00
let cargoSequence = 0
2026-09-08 20:28:05 +08:00
let skuRelabelSequence = 0
2026-08-21 17:20:51 +08:00
watch(() => props.service, () => { mode.value = 'list'; resetFilters(); resetForm(); void load() })
2026-09-13 21:10:14 +08:00
watch(() => form.relabel, (value) => {
if (props.service === 'transfer' && value) {
labelingMode.value = 'carton_and_sku'
if (!transferRelabelRows.value.length) addTransferRelabelRow()
}
})
2026-09-09 13:35:51 +08:00
watch(labelingMode, (value) => { if (value !== 'carton' && !skuRelabelRows.value.length) addSkuRelabelRow() })
2026-08-21 17:20:51 +08:00
onMounted(() => { void load() })
function resetFilters() { Object.assign(filters, { number: '', inboundNo: '', warehouse: '', destination: '', status: '', start: '', end: '' }) }
function resetForm() {
2026-09-13 21:10:14 +08:00
Object.assign(form, { customerNo: '', inboundNo: '', sourceWarehouse: '', warehouse: '', destination: '', address: '', contact: '', phone: '', deliveryMethod: '', relabelDeliveryMethod: '', trackingNo: '', reason: '', requirement: '', relabel: false, remark: '', labelDeadline: '', estimatedArrivalAt: '', estimatedShipAt: '', fbaShipmentNo: '', volumeCbm: 0.01, chargeAmount: 0 })
2026-08-21 17:20:51 +08:00
cargoRows.value = []
2026-09-08 20:28:05 +08:00
labelingMode.value = 'carton'
skuRelabelRows.value = []
2026-09-10 17:12:52 +08:00
importedRelabelBindings.value = []
2026-08-21 17:20:51 +08:00
discardCandidates.value = []
selectedDiscardOrderId.value = ''
2026-09-08 20:28:05 +08:00
relabelCandidates.value = []
selectedRelabelInboundOrderId.value = ''
2026-09-10 15:34:03 +08:00
transferHasFbaDestination.value = false
selectedOperationWarehouse.value = null
2026-08-21 17:20:51 +08:00
}
async function openCreate() {
resetForm(); mode.value = 'create'
2026-09-08 20:28:05 +08:00
if (props.service === 'labeling' || props.service === 'relabel' || props.service === 'transfer') void loadWarehouses()
if (props.service === 'labeling') return
2026-08-21 17:20:51 +08:00
if (props.service !== 'disposal') { addCargoRow(); return }
discardCandidatesLoading.value = true
try { discardCandidates.value = await listDiscardCandidates() }
catch (error) { ElMessage.error(error instanceof Error ? error.message : '可废弃入库单加载失败') }
finally { discardCandidatesLoading.value = false }
}
2026-09-13 21:10:14 +08:00
function createCargoRow(): CargoRow { return { id: ++cargoSequence, sku: '', name: '', quantity: 1, fnsku: '', shipmentNo: '', newSkuCode: '', shippingMark: '', customerCartonMarks: '', cartonMark: '', fbaCartonMark: '', destinationWarehouseId: '', remark: '' } }
function addCargoRow() { cargoRows.value.push(createCargoRow()) }
function addTransferRelabelRow() { transferRelabelRows.value.push(createCargoRow()) }
function clearTransferRelabelRows() {
transferRelabelRows.value = []
}
2026-09-09 13:35:51 +08:00
function addSkuRelabelRow() {
const source = cargoRows.value[Math.min(skuRelabelRows.value.length, Math.max(0, cargoRows.value.length - 1))]
skuRelabelRows.value.push({ id: ++skuRelabelSequence, sourceRowId: source?.id || null,
cartonMark: '', originalSku: source?.sku || '', newSku: '', quantity: 1,
fbaCartonMark: '', destinationWarehouseId: source?.destinationWarehouseId || '', remark: '' })
2026-09-08 20:28:05 +08:00
}
2026-09-09 13:35:51 +08:00
function removeSkuRelabelRow(id: number) { skuRelabelRows.value = skuRelabelRows.value.filter((row) => row.id !== id) }
2026-09-10 17:12:52 +08:00
function applyExistingSkuBinding(row: SkuRelabelRow) {
if (!row.originalSku.trim() || row.newSku.trim()) return
const mapping = skuRelabelRows.value.find((item) => item.id !== row.id
&& item.originalSku.trim().toUpperCase() === row.originalSku.trim().toUpperCase() && item.newSku.trim())
if (mapping) row.newSku = mapping.newSku
}
function applyExistingCartonBinding(row: SkuRelabelRow) {
if (!row.cartonMark.trim()) return
const cartonMark = row.cartonMark.trim().toUpperCase()
const mapping = cargoRows.value.find((item) => item.cartonMark.trim().toUpperCase() === cartonMark && item.fbaCartonMark.trim())
|| skuRelabelRows.value.find((item) => item.id !== row.id
&& item.cartonMark.trim().toUpperCase() === cartonMark && item.fbaCartonMark.trim())
if (!mapping) return
if (!row.fbaCartonMark.trim()) row.fbaCartonMark = mapping.fbaCartonMark
if (!row.destinationWarehouseId) row.destinationWarehouseId = mapping.destinationWarehouseId
}
2026-09-10 15:34:03 +08:00
function selectShippingLabel(event: Event) { shippingLabelFile.value = (event.target as HTMLInputElement).files?.[0] || null }
2026-09-08 20:28:05 +08:00
function buildSkuRelabelItems(): RelabelCreate['items'] {
2026-09-09 13:35:51 +08:00
const groups = new Map<string, SkuRelabelRow[]>()
2026-09-08 20:28:05 +08:00
for (const row of skuRelabelRows.value) {
2026-09-09 13:35:51 +08:00
const cartonMark = row.cartonMark.trim().toUpperCase()
if (!cartonMark) throw new Error('请填写每个箱子的我方箱唛')
const rows = groups.get(cartonMark) || []
2026-09-08 20:28:05 +08:00
rows.push(row)
2026-09-09 13:35:51 +08:00
groups.set(cartonMark, rows)
2026-09-08 20:28:05 +08:00
}
2026-09-09 13:35:51 +08:00
const anchors = cargoRows.value.flatMap((item) => Array.from(
{ length: Number(item.availableQuantity || 0) }, () => item,
))
if (!anchors.length) throw new Error('请先选择有库存的历史入仓单')
if (groups.size > anchors.length) {
throw new Error('填写的箱唛数量超过当前入仓单可用库存箱数')
}
return [...groups.entries()].map(([_, rows], groupIndex) => {
2026-09-10 17:12:52 +08:00
const source = cargoRows.value.find((item) => item.id === rows[0].sourceRowId) || anchors[groupIndex]
2026-09-08 20:28:05 +08:00
if (!source?.sourceInventoryBalanceId || !source.sourceInventoryPositionId || !source.sourceInboundOrderId) {
throw new Error('来源库存已变化,请重新选择有库存的历史入仓单')
}
const cartonMark = rows[0].cartonMark.trim()
2026-09-09 13:35:51 +08:00
const changeCarton = labelingMode.value === 'carton_and_sku'
2026-09-08 20:28:05 +08:00
const fbaCartonMark = rows[0].fbaCartonMark.trim()
const destinationWarehouseId = rows[0].destinationWarehouseId
if (changeCarton && (!fbaCartonMark || !destinationWarehouseId
|| rows.some((row) => row.fbaCartonMark.trim() !== fbaCartonMark || row.destinationWarehouseId !== destinationWarehouseId))) {
throw new Error('同一我方箱唛的组合换标必须使用相同的新 FBA 箱唛和目的仓')
}
return {
sourceInventoryBalanceId: source.sourceInventoryBalanceId,
sourceInventoryPositionId: source.sourceInventoryPositionId,
sourceInboundOrderId: source.sourceInboundOrderId,
skuCode: source.sku || rows[0].originalSku,
productNameZh: source.name,
2026-09-09 13:35:51 +08:00
totalPackageCount: 1,
notifiedPackageCount: 1,
2026-09-08 20:28:05 +08:00
shipmentNo: source.shipmentNo || cartonMark,
trackingCode: form.trackingNo || undefined,
newSkuCode: rows[0].newSku,
labelRequirement: form.requirement || undefined,
cartonMark,
fbaCartonMark: changeCarton ? fbaCartonMark : undefined,
destinationFbaWarehouseCode: changeCarton ? destinationWarehouseId : undefined,
operationType: changeCarton ? 'CARTON_AND_SKU' as const : 'SKU_LABEL' as const,
skuItems: rows.map((row) => ({ skuCode: row.originalSku.trim(), newSkuCode: row.newSku.trim(), quantity: row.quantity, remark: row.remark || undefined })),
remark: undefined,
}
})
}
function addRelabelCargoRow() {
if (!selectedRelabelInboundOrderId.value) { ElMessage.warning('请先选择有库存的历史入仓单'); return }
2026-09-10 17:12:52 +08:00
const source = cargoRows.value.find((row) => !row.included)
if (!source) { ElMessage.warning('所选入仓单没有更多可添加的库存箱'); return }
source.included = true
2026-09-08 20:28:05 +08:00
}
function removeCargoRow(id: number) {
2026-09-10 17:12:52 +08:00
const item = cargoRows.value.find((row) => row.id === id)
if (props.service === 'labeling' && item?.sourceInventoryBalanceId) {
Object.assign(item, { included: false, shipmentNo: '', cartonMark: '', fbaCartonMark: '', destinationWarehouseId: '', remark: '' })
return
}
2026-09-08 20:28:05 +08:00
cargoRows.value = cargoRows.value.filter((row) => row.id !== id)
}
2026-09-10 17:12:52 +08:00
function clearRelabelRows() {
for (const row of cargoRows.value) Object.assign(row, { included: false, shipmentNo: '', cartonMark: '', fbaCartonMark: '', destinationWarehouseId: '', remark: '' })
}
2026-09-08 20:28:05 +08:00
function csvCell(value: string | number | undefined) { return `"${String(value ?? '').replaceAll('"', '""')}"` }
2026-09-13 21:10:14 +08:00
function transferCustomerMarks(row: CargoRow) {
return row.customerCartonMarks.split(/[\r\n,,]+/).map((value) => value.trim()).filter(Boolean)
}
function downloadTransferCartonMarkTemplate() {
if (!cargoRows.value.length || cargoRows.value.some((row) => !row.sku.trim())) {
ElMessage.warning('请先填写完整的 SKU 明细'); return
}
const header = ['SKU', '客户箱唛号']
const rows = cargoRows.value.flatMap((row) => Array.from({ length: row.quantity }, () => [row.sku, '']))
const blob = new Blob([`\uFEFF${[header, ...rows].map((row) => row.map(csvCell).join(',')).join('\r\n')}\r\n`], { type: 'text/csv;charset=utf-8' })
const url = URL.createObjectURL(blob); const anchor = document.createElement('a')
anchor.href = url; anchor.download = '仓储转运-客户箱唛模板.csv'; anchor.click(); URL.revokeObjectURL(url)
}
async function importTransferCartonMarks(event: Event) {
const input = event.target as HTMLInputElement; const file = input.files?.[0]; input.value = ''
if (!file) return
const lines = (await file.text()).replace(/^\uFEFF/, '').split(/\r?\n/).filter((line) => line.trim())
if (lines.length < 2) { ElMessage.error('模板中没有可导入的客户箱唛'); return }
const headers = parseCsvLine(lines[0])
if (!headers.includes('SKU') || !headers.includes('客户箱唛号')) { ElMessage.error('客户箱唛模板格式不正确,请重新下载模板'); return }
const imported = lines.slice(1).map(parseCsvLine).map((values) => Object.fromEntries(headers.map((key, index) => [key, values[index] || '']))).filter((item) => item.SKU || item['客户箱唛号'])
if (!imported.length || imported.some((item) => !item.SKU || !item['客户箱唛号'])) { ElMessage.error('SKU 和客户箱唛号必须填写完整'); return }
const marks = new Set<string>()
for (const item of imported) {
const row = cargoRows.value.find((candidate) => candidate.sku.trim().toUpperCase() === item.SKU.trim().toUpperCase())
if (!row) { ElMessage.error(`SKU ${item.SKU} 不属于当前转运单`); return }
const mark = item['客户箱唛号'].trim()
if (marks.has(mark.toUpperCase())) { ElMessage.error(`客户箱唛 ${mark} 不能重复`); return }
marks.add(mark.toUpperCase())
}
for (const row of cargoRows.value) row.customerCartonMarks = imported.filter((item) => item.SKU.trim().toUpperCase() === row.sku.trim().toUpperCase()).map((item) => item['客户箱唛号'].trim()).join('\n')
ElMessage.success(`已导入 ${imported.length} 个客户箱唛`)
}
2026-09-10 17:12:52 +08:00
function downloadRelabelTemplate(templateType: RelabelTemplateType) {
2026-09-08 20:28:05 +08:00
if (!cargoRows.value.length) { ElMessage.warning('请先选择有库存的历史入仓单'); return }
2026-09-10 17:12:52 +08:00
const cartonTemplate = templateType === 'carton'
const mixedTemplate = templateType === 'mixed'
2026-09-10 15:56:39 +08:00
const header = cartonTemplate
2026-09-10 17:12:52 +08:00
? ['明细行标识', '旧箱唛', '新FBA箱唛', 'FBA目的仓', 'FBA货件编号', '备注']
: ['明细行标识', '旧箱唛', '旧SKU标签', '新SKU标签', '换标数量(件)',
...(mixedTemplate ? ['新FBA箱唛', 'FBA目的仓', 'FBA货件编号'] : []), '备注']
const rows = cargoRows.value.map((item) => cartonTemplate
? [item.id, item.cartonMark, item.fbaCartonMark, item.destinationWarehouseId, item.shipmentNo, item.remark]
: [item.id, item.cartonMark, item.sku, '', 1,
...(mixedTemplate ? [item.fbaCartonMark, item.destinationWarehouseId, item.shipmentNo] : []), item.remark])
2026-09-08 20:28:05 +08:00
const blob = new Blob([`\uFEFF${[header, ...rows].map((row) => row.map(csvCell).join(',')).join('\r\n')}\r\n`], { type: 'text/csv;charset=utf-8' })
2026-09-06 15:53:56 +08:00
const url = URL.createObjectURL(blob)
const anchor = document.createElement('a')
2026-09-10 15:56:39 +08:00
anchor.href = url
2026-09-10 17:12:52 +08:00
anchor.download = cartonTemplate ? '贴标申请-箱号关联模板.csv'
: mixedTemplate ? '贴标申请-SKU混箱换标模板.csv' : '贴标申请-SKU关联模板.csv'
2026-09-10 15:56:39 +08:00
anchor.click(); URL.revokeObjectURL(url)
2026-09-06 15:53:56 +08:00
}
function parseCsvLine(line: string) {
const values: string[] = []; let value = ''; let quoted = false
for (let index = 0; index < line.length; index += 1) {
const char = line[index]
if (char === '"') { if (quoted && line[index + 1] === '"') { value += '"'; index += 1 } else quoted = !quoted }
else if (char === ',' && !quoted) { values.push(value.trim()); value = '' }
else value += char
}
values.push(value.trim()); return values
}
2026-09-10 17:12:52 +08:00
async function importRelabelTemplate(event: Event, templateType: RelabelTemplateType) {
2026-09-06 15:53:56 +08:00
const input = event.target as HTMLInputElement; const file = input.files?.[0]; input.value = ''
if (!file) return
const lines = (await file.text()).replace(/^\uFEFF/, '').split(/\r?\n/).filter((line) => line.trim())
if (lines.length < 2) { ElMessage.error('模板中没有可导入的明细'); return }
2026-09-08 20:28:05 +08:00
const headers = parseCsvLine(lines[0])
2026-09-10 17:12:52 +08:00
if (templateType !== 'carton') {
const mixedTemplate = templateType === 'mixed'
const required = ['明细行标识', '旧箱唛', '旧SKU标签', '新SKU标签', '换标数量(件)',
...(mixedTemplate ? ['新FBA箱唛', 'FBA目的仓', 'FBA货件编号'] : [])]
2026-09-10 15:56:39 +08:00
if (required.some((name) => !headers.includes(name))) {
2026-09-10 17:12:52 +08:00
ElMessage.error('SKU 关联模板格式不正确,请重新下载 SKU 关联模板'); return
2026-09-10 15:56:39 +08:00
}
const imported = lines.slice(1).map(parseCsvLine)
.map((values) => Object.fromEntries(headers.map((key, index) => [key, values[index] || ''])))
2026-09-10 17:12:52 +08:00
.filter((item) => item['旧箱唛'] || item['新SKU标签'] || item['新FBA箱唛'])
2026-09-10 15:56:39 +08:00
if (!imported.length || imported.some((item) => required.some((key) => !item[key]))) {
2026-09-10 17:12:52 +08:00
ElMessage.error('旧箱唛、旧 SKU 标签、新 SKU 标签和换标数量必须填写完整'); return
2026-09-10 15:56:39 +08:00
}
2026-09-10 17:12:52 +08:00
const rows: ImportedRelabelBinding[] = []
2026-09-10 15:56:39 +08:00
for (const item of imported) {
const quantity = Number(item['换标数量(件)'])
if (!Number.isInteger(quantity) || quantity < 1) {
2026-09-10 17:12:52 +08:00
ElMessage.error(`箱唛 ${item['旧箱唛']} 的换标数量必须是大于 0 的整数`); return
2026-09-10 15:56:39 +08:00
}
2026-09-10 17:12:52 +08:00
const source = cargoRows.value.find((row) => String(row.id) === item['明细行标识'])
if (!source) { ElMessage.error(`明细行 ${item['明细行标识']} 不属于当前库存,请重新下载模板`); return }
2026-09-10 15:56:39 +08:00
rows.push({
id: ++skuRelabelSequence,
2026-09-10 17:12:52 +08:00
sourceRowId: source.id,
cartonMark: item['旧箱唛'].trim(),
2026-09-10 15:56:39 +08:00
originalSku: item['旧SKU标签'].trim(),
newSku: item['新SKU标签'].trim(),
quantity,
2026-09-10 17:12:52 +08:00
fbaCartonMark: mixedTemplate ? item['新FBA箱唛'].trim() : '',
destinationWarehouseId: mixedTemplate ? item['FBA目的仓'].trim() : '',
2026-09-10 15:56:39 +08:00
remark: item['备注'] || '',
2026-09-10 17:12:52 +08:00
templateType,
selected: true,
shipmentNo: mixedTemplate ? item['FBA货件编号'].trim() : '',
2026-09-10 15:56:39 +08:00
})
}
2026-09-10 17:12:52 +08:00
importedRelabelBindings.value = rows
ElMessage.success(`已读取 ${rows.length} 条绑定关系,请选择后导入到明细`)
2026-09-10 15:56:39 +08:00
return
}
2026-09-10 17:12:52 +08:00
const required = ['明细行标识', '旧箱唛', '新FBA箱唛', 'FBA目的仓', 'FBA货件编号']
if (required.some((name) => !headers.includes(name))) { ElMessage.error('箱号关联模板格式不正确,请重新下载箱号关联模板'); return }
2026-09-08 20:28:05 +08:00
const imported = lines.slice(1).map(parseCsvLine)
.map((values) => Object.fromEntries(headers.map((key, index) => [key, values[index] || ''])))
2026-09-10 17:12:52 +08:00
.filter((item) => item['旧箱唛'] || item['新FBA箱唛'])
2026-09-08 20:28:05 +08:00
if (!imported.length || imported.some((item) => required.some((key) => !item[key]))) {
2026-09-10 17:12:52 +08:00
ElMessage.error('模板中的明细行、旧箱唛、新 FBA 箱唛、FBA 目的仓和货件编号必须填写完整'); return
2026-09-08 20:28:05 +08:00
}
if (new Set(imported.map((item) => item['明细行标识'])).size !== imported.length) {
ElMessage.error('模板中的明细行标识不能重复'); return
}
const importedByRow = new Map(imported.map((item) => [item['明细行标识'], item]))
2026-09-10 17:12:52 +08:00
if (importedByRow.size !== imported.length || [...importedByRow.keys()].some((id) => !cargoRows.value.some((row) => String(row.id) === id))) {
ElMessage.error('模板存在重复或不属于当前库存的明细行,请重新下载模板'); return
2026-09-08 20:28:05 +08:00
}
2026-09-10 17:12:52 +08:00
const bindings: ImportedRelabelBinding[] = []
for (const [rowId, item] of importedByRow) {
const row = cargoRows.value.find((candidate) => String(candidate.id) === rowId)!
bindings.push({ id: ++skuRelabelSequence, sourceRowId: row.id, cartonMark: item['旧箱唛'],
originalSku: row.sku, newSku: '', quantity: 1, fbaCartonMark: item['新FBA箱唛'],
destinationWarehouseId: item['FBA目的仓'], remark: item['备注'] || '', templateType,
selected: true, shipmentNo: item['FBA货件编号'] })
}
importedRelabelBindings.value = bindings
ElMessage.success(`已读取 ${bindings.length} 条箱号关联,请选择后导入到明细`)
}
function selectAllImportedBindings(value: boolean) {
for (const item of importedRelabelBindings.value) item.selected = value
}
function importSelectedBindingsToDetails() {
const selected = importedRelabelBindings.value.filter((item) => item.selected)
if (!selected.length) { ElMessage.warning('请至少选择一条绑定关系'); return }
const templateType = selected[0].templateType
if (selected.some((item) => item.templateType !== templateType)) { ElMessage.warning('请重新上传同一种绑定模板'); return }
if (templateType === 'carton') {
for (const binding of selected) {
const source = cargoRows.value.find((row) => row.id === binding.sourceRowId)
if (!source) continue
Object.assign(source, { included: true, quantity: 1, shipmentNo: binding.shipmentNo,
cartonMark: binding.cartonMark, fbaCartonMark: binding.fbaCartonMark,
destinationWarehouseId: binding.destinationWarehouseId, remark: binding.remark })
2026-09-08 20:28:05 +08:00
}
2026-09-10 17:12:52 +08:00
labelingMode.value = 'carton'
} else {
const importedRows = selected.map(({ templateType: _, selected: __, shipmentNo, ...item }) => item)
for (const item of importedRows) {
skuRelabelRows.value = skuRelabelRows.value.filter((row) => !(row.cartonMark.trim().toUpperCase() === item.cartonMark.trim().toUpperCase()
&& row.originalSku.trim().toUpperCase() === item.originalSku.trim().toUpperCase()))
skuRelabelRows.value.push(item)
if (templateType === 'mixed') {
const source = cargoRows.value.find((row) => row.id === item.sourceRowId)
const shipmentNo = selected.find((binding) => binding.id === item.id)?.shipmentNo || ''
if (source) Object.assign(source, { included: true, cartonMark: item.cartonMark,
fbaCartonMark: item.fbaCartonMark, destinationWarehouseId: item.destinationWarehouseId, shipmentNo })
}
}
labelingMode.value = templateType === 'mixed' ? 'carton_and_sku' : 'sku'
2026-09-08 20:28:05 +08:00
}
2026-09-10 17:12:52 +08:00
importedRelabelBindings.value = importedRelabelBindings.value.filter((item) => !item.selected)
ElMessage.success(`已将 ${selected.length} 条绑定关系导入本次换标明细`)
2026-09-06 15:53:56 +08:00
}
2026-08-21 17:20:51 +08:00
function selectDiscardInbound(orderId: string) {
const candidate = discardCandidates.value.find((item) => item.orderId === orderId)
if (!candidate) { cargoRows.value = []; return }
form.inboundNo = candidate.inboundCode
form.warehouse = candidate.overseasWarehouseId
cargoRows.value = candidate.skus.map((sku) => ({
id: ++cargoSequence, sku: sku.skuCode, name: sku.productName, quantity: 0,
availableQuantity: sku.availablePackages, sourceOrderId: candidate.orderId,
2026-09-13 21:10:14 +08:00
fnsku: '', shipmentNo: '', newSkuCode: '', shippingMark: '', customerCartonMarks: '', cartonMark: '', fbaCartonMark: '', destinationWarehouseId: '', remark: '',
2026-08-21 17:20:51 +08:00
}))
}
function dateTime(value: string, end = false) { return value ? `${value}T${end ? '23:59:59' : '00:00:00'}` : undefined }
function row(values: Array<string | number | undefined | null>) { return Object.fromEntries(values.map((value, index) => [String(index), value == null || value === '' ? '—' : String(value)])) }
2026-09-06 15:53:56 +08:00
function warehouseDisplay(value?: string) {
if (!value) return '—'
2026-09-08 20:28:05 +08:00
return value.split(',').map((code) => {
const warehouse = [...operationWarehouses.value, ...fbaWarehouses.value].find((item) => String(item.id) === code || item.warehouseCode === code)
return warehouse ? warehouseLabel(warehouse) : code
}).join('、')
2026-09-06 15:53:56 +08:00
}
2026-09-07 19:04:00 +08:00
function statusLabel(value: string) {
const labels: Record<string, string> = {
2026-09-10 15:34:03 +08:00
PENDING_PROCESSING: '待处理', PROCESSING: '处理中', CUSTOMER_CONFIRM_PENDING: '待客户确认',
READY_FOR_OUTBOUND: '待出库', COMPLETED: '已完成', CANCELLED: '已取消',
2026-09-07 19:04:00 +08:00
PENDING_REVIEW: '待审核', APPROVED: '已通过', REJECTED: '已拒绝', PENDING_ASSIGNMENT: '待分配',
ALLOCATED: '已分配', OUTBOUNDED: '已出库', PARTIALLY_RECEIVED: '部分收货', RECEIVED: '已收货',
2026-09-07 19:04:00 +08:00
}
return labels[value] || value || '—'
}
function deliveryMethodLabel(value: string) {
2026-09-10 15:34:03 +08:00
const labels: Record<string, string> = {
TRUCK: '卡车派送', EXPRESS: '快递派送', TRUCK_PICKUP: '卡车自提', EXPRESS_PICKUP: '快递自提',
WAREHOUSE_PICKUP: '仓库上门提货', CUSTOMER_DELIVERY: '客户送货到仓',
// Keep the legacy value readable for historical applications created before the four-way split.
PICKUP: '仓库自提',
}
2026-09-07 19:04:00 +08:00
return labels[value] || value || '—'
}
2026-09-10 15:34:03 +08:00
function formatDateTime(value?: string | null) {
2026-09-07 19:04:00 +08:00
return value ? value.replace('T', ' ').slice(0, 19) : '—'
}
2026-08-21 17:20:51 +08:00
async function load() {
loading.value = true
try {
if (props.service === 'labeling') {
const result = await listRelabels({ page: 1, size: 20, relabelNo: filters.number || undefined, overseasWarehouseId: filters.warehouse || undefined, status: filters.status || undefined, createdFrom: dateTime(filters.start), createdTo: dateTime(filters.end, true) })
2026-09-06 15:53:56 +08:00
submittedRows.value = result.records.map((item) => {
const destinationWarehouses = warehouseDisplay(item.overseasWarehouseId)
2026-09-08 20:28:05 +08:00
return row([item.relabelNo, item.customerOrderNo, destinationWarehouses, '—', item.itemCount, item.totalPackageCount, statusLabel(item.status), formatDateTime(item.createdAt)])
2026-09-06 15:53:56 +08:00
})
2026-08-21 17:20:51 +08:00
} else if (props.service === 'disposal') {
const result = await listDiscards({ page: 1, size: 20, 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) })
2026-09-08 20:28:05 +08:00
submittedRows.value = result.records.map((item) => row([item.discardNo, item.inboundCodes, item.overseasWarehouseName || '—', '—', item.totalPackages, `${item.currency} ${item.totalFee}`, statusLabel(item.status), formatDateTime(item.createdAt)]))
2026-08-21 17:20:51 +08:00
} else {
const result = await listTransfers({ page: 1, size: 20, transferNo: filters.number || undefined, inboundCode: filters.inboundNo || undefined, operatingWarehouseId: filters.warehouse || undefined, fbaWarehouseCode: filters.destination || undefined, status: filters.status || undefined, needRelabel: props.service === 'relabel', createdFrom: dateTime(filters.start), createdTo: dateTime(filters.end, true) })
submittedRows.value = result.records.map((item) => props.service === 'transfer'
? row([item.transferNo, item.packageCount, '—', item.fbaWarehouseCode, deliveryMethodLabel(item.deliveryMethod), item.needRelabel ? '是 · 待绑定换标单' : '否', statusLabel(item.status), formatDateTime(item.createdAt)])
2026-09-08 20:28:05 +08:00
: row([item.transferNo, '—', item.fbaWarehouseCode, '—', item.packageCount, '是', item.relabelNo ? '已生成' : '待生成', statusLabel(item.status), formatDateTime(item.createdAt)]))
2026-08-21 17:20:51 +08:00
}
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '列表加载失败') }
finally { loading.value = false }
}
function query() { void load() }
async function submit() {
const primaryWarehouse = props.service === 'relabel' ? form.sourceWarehouse : form.warehouse
2026-09-10 17:12:52 +08:00
const relabelRows = labelingMode.value === 'carton' ? cartonDetailRows.value : skuRelabelRows.value
2026-08-21 17:20:51 +08:00
const invalidCargo = props.service === 'disposal'
? !selectedDiscardOrderId.value || cargoRows.value.every((item) => item.quantity < 1)
|| cargoRows.value.some((item) => item.quantity < 0 || item.quantity > Number(item.availableQuantity || 0))
2026-09-13 21:10:14 +08:00
: (props.service === 'labeling' || props.service === 'relabel') && (relabelRows.length === 0 || (props.service === 'labeling' && labelingMode.value !== 'carton'
2026-09-09 13:35:51 +08:00
? skuRelabelRows.value.some((item) => !item.cartonMark || !item.originalSku || !item.newSku || item.quantity < 1
|| (labelingMode.value === 'carton_and_sku' && (!item.fbaCartonMark || !item.destinationWarehouseId)))
2026-09-10 17:12:52 +08:00
: cargoRows.value.some((item) => (props.service !== 'labeling' && !item.sku) || !item.name
2026-09-13 21:10:14 +08:00
|| (props.service !== 'labeling' && item.quantity < 1))))
if ((props.service === 'relabel' && !form.inboundNo) || (props.service !== 'disposal' && !primaryWarehouse) || invalidCargo) {
2026-08-21 17:20:51 +08:00
ElMessage.warning('请填写必填信息和完整的货物明细'); return
}
2026-09-08 20:28:05 +08:00
if (props.service === 'labeling' && (!form.labelDeadline || !form.contact || !form.phone || form.chargeAmount <= 0)) {
ElMessage.warning('请填写贴标截止时间、联系人和贴标费用'); return
2026-09-06 15:53:56 +08:00
}
2026-09-10 15:34:03 +08:00
if (props.service === 'labeling' && labelingMode.value !== 'sku' && !form.deliveryMethod) {
ElMessage.warning('请选择换标完成后的派送方式'); return
}
if (props.service === 'labeling' && labelingMode.value !== 'sku' && (form.deliveryMethod === 'EXPRESS' || form.deliveryMethod === 'EXPRESS_PICKUP')
&& form.relabel && !shippingLabelFile.value) {
ElMessage.warning('选择打印面单时必须上传面单文件'); return
}
2026-09-10 17:12:52 +08:00
if (props.service === 'labeling' && labelingMode.value === 'carton' && cartonDetailRows.value.some((item) => !item.shipmentNo || !item.cartonMark || !item.fbaCartonMark)) {
2026-09-06 15:53:56 +08:00
ElMessage.warning('请上传关联面单,确保每箱的我方箱唛和 FBA 箱唛完整'); return
}
2026-09-10 17:12:52 +08:00
if (props.service === 'labeling' && labelingMode.value === 'carton' && cartonDetailRows.value.some((item) => !item.destinationWarehouseId || !item.sourceInventoryPositionId)) {
2026-09-08 20:28:05 +08:00
ElMessage.warning('请选择有库存的历史入仓单,并为每项填写 FBA 目的仓'); return
2026-08-21 17:20:51 +08:00
}
if (props.service === 'disposal' && !form.reason) {
ElMessage.warning('请选择废弃原因'); return
}
2026-09-10 15:34:03 +08:00
if (props.service === 'relabel' && (!form.destination || !form.address || !form.deliveryMethod || !form.estimatedArrivalAt || !form.fbaShipmentNo || form.volumeCbm <= 0)) {
2026-08-21 17:20:51 +08:00
ElMessage.warning('请填写完整的派送信息、体积、预计到达时间和 FBA 货件号'); return
}
2026-09-13 21:10:14 +08:00
if (props.service === 'transfer' && (!form.warehouse || !form.deliveryMethod || !form.address || !form.estimatedArrivalAt || !form.estimatedShipAt || form.volumeCbm < 0)) {
const missing = [!form.warehouse && '中转仓', !form.deliveryMethod && '送货方式', !form.address && '地址', !form.estimatedArrivalAt && '预计入仓时间', !form.estimatedShipAt && '预计发货时间', form.volumeCbm < 0 && '预估总体积'].filter(Boolean).join('、')
ElMessage.warning(`请填写:${missing || '必填信息'}`); return
2026-09-10 15:34:03 +08:00
}
if (props.service === 'transfer' && transferHasFbaDestination.value && (!form.destination || !form.fbaShipmentNo)) {
ElMessage.warning('已选择有 FBA 去向,请填写 FBA 目的仓和货件编号'); return
}
2026-09-13 21:10:14 +08:00
if (props.service === 'transfer' && form.relabel) {
if (!form.labelDeadline || !form.contact || !form.phone || form.chargeAmount < 0.01 || (labelingMode.value !== 'sku' && !form.relabelDeliveryMethod)) { ElMessage.warning('请填写换标截止时间、联系人、费用和换标后派送方式'); return }
const relabelRows = transferRelabelRows.value
const allMarks = relabelRows.flatMap(transferCustomerMarks)
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 (allMarks.join(',').length > 2000 || relabelRows.some((row) => transferCustomerMarks(row).join(',').length > 255)) { ElMessage.warning('客户箱唛内容过长,请减少单次提交箱数或缩短箱唛号'); return }
if (relabelRows.some((row) => row.quantity < 1 || (labelingMode.value !== 'carton' && !row.newSkuCode.trim()) || (labelingMode.value !== 'sku' && (!row.fbaCartonMark.trim() || !row.destinationWarehouseId.trim())))) { ElMessage.warning('请按当前换标类型完整填写换标明细'); return }
}
2026-08-21 17:20:51 +08:00
try { await ElMessageBox.confirm(`确认提交${config.value.title}吗?`, '提交确认', { confirmButtonText: '提交', cancelButtonText: '取消' }) } catch { return }
saving.value = true
try {
const idempotencyKey = crypto.randomUUID()
if (props.service === 'labeling') {
2026-09-08 20:28:05 +08:00
const sourceInboundOrders = [...new Set(cargoRows.value.map((item) => item.sourceInboundOrderNo || item.sourceInboundCode).filter((value): value is string => Boolean(value)))].join('、').slice(0, 128)
const items = labelingMode.value === 'carton'
2026-09-10 17:12:52 +08:00
? cartonDetailRows.value.map((item) => ({ sourceInventoryBalanceId: item.sourceInventoryBalanceId!, sourceInventoryPositionId: item.sourceInventoryPositionId!, sourceInboundOrderId: item.sourceInboundOrderId!, skuCode: item.sku, productNameZh: item.name, totalPackageCount: 1, notifiedPackageCount: 1, shipmentNo: item.shipmentNo, trackingCode: form.trackingNo || undefined, newSkuCode: undefined, labelRequirement: form.requirement || undefined, cartonMark: item.cartonMark, fbaCartonMark: item.fbaCartonMark, destinationFbaWarehouseCode: item.destinationWarehouseId, operationType: 'CARTON_LABEL' as const, skuItems: [], remark: item.remark || undefined }))
2026-09-08 20:28:05 +08:00
: buildSkuRelabelItems()
2026-09-10 15:34:03 +08:00
const created = await createRelabel({ customerOrderNo: sourceInboundOrders || '贴标申请', operationWarehouseId: form.warehouse, labelDeadline: form.labelDeadline, contactName: form.contact, contactPhone: form.phone, chargeAmount: form.chargeAmount, remark: form.remark || undefined, idempotencyKey, items,
deliveryMethod: labelingMode.value === 'sku' ? undefined : form.deliveryMethod as 'TRUCK' | 'EXPRESS' | 'TRUCK_PICKUP' | 'EXPRESS_PICKUP',
printShippingLabel: labelingMode.value !== 'sku' && (form.deliveryMethod === 'EXPRESS' || form.deliveryMethod === 'EXPRESS_PICKUP') && form.relabel })
if (shippingLabelFile.value) await uploadRelabelShippingLabel(created.application.relabelNo, shippingLabelFile.value)
2026-08-21 17:20:51 +08:00
} else if (props.service === 'disposal') {
const items = cargoRows.value.filter((item) => item.quantity > 0).map((item) => {
if (!item.sourceOrderId || item.sourceOrderId !== selectedDiscardOrderId.value || item.quantity > Number(item.availableQuantity || 0)) {
throw new Error(`SKU ${item.sku} 的可废弃库存不足`)
}
return { orderId: item.sourceOrderId, skuCode: item.sku, discardPackages: item.quantity }
})
await createDiscard({ items, idempotencyKey, remark: [form.reason, form.remark].filter(Boolean).join(';') || undefined })
} else {
const packageCount = cargoRows.value.reduce((sum, item) => sum + item.quantity, 0)
2026-09-13 21:10:14 +08:00
const customerCartonMarks = (form.relabel ? transferRelabelRows.value : cargoRows.value).flatMap(transferCustomerMarks)
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'
await createTransfer({ operatingWarehouseId: primaryWarehouse, fbaWarehouseCode: form.destination || undefined, deliveryAddress: form.address, packageCount, volumeCbm: form.volumeCbm, deliveryMethod: form.deliveryMethod, estimatedArrivalAt: form.estimatedArrivalAt, estimatedShipAt: form.estimatedShipAt || undefined, fbaShipmentNo: form.fbaShipmentNo || 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, relabelContactName: form.relabel ? form.contact : undefined, relabelContactPhone: form.relabel ? form.phone : undefined, relabelChargeAmount: form.relabel ? form.chargeAmount : undefined, relabelDeliveryMethod: form.relabel && labelingMode.value !== 'sku' ? form.relabelDeliveryMethod as 'TRUCK' | 'EXPRESS' | 'TRUCK_PICKUP' | 'EXPRESS_PICKUP' : undefined, idempotencyKey, items: cargoRows.value.map((item) => ({ skuCode: item.sku, productNameZh: item.name, packageCount: item.quantity, 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: item.destinationWarehouseId || undefined, remark: item.remark || undefined })) : undefined })
2026-08-21 17:20:51 +08:00
}
ElMessage.success(`${config.value.title}提交成功`); mode.value = 'list'; await load()
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '提交失败') }
finally { saving.value = false }
}
async function openDetail(value: Record<string, string>) {
try {
const detail = props.service === 'labeling' ? await getRelabel(value['0']) : props.service === 'disposal' ? await getDiscard(value['0']) : await getTransfer(value['0'])
2026-09-13 21:10:14 +08:00
selectedDetail.value = { ...value, ...(detail as Record<string, unknown>) as Record<string, string> }
2026-09-06 18:11:42 +08:00
selectedRelabelDetail.value = props.service === 'labeling' ? detail as RelabelDetail : null
mode.value = 'detail'
2026-08-21 17:20:51 +08:00
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '详情加载失败') }
}
2026-09-13 21:10:14 +08:00
async function confirmTransferMeasurements() {
const transferNo = String(selectedDetail.value?.transferNo || selectedDetail.value?.['0'] || '')
if (!transferNo) return
try { await ElMessageBox.confirm('确认 PDA 回传的箱数、尺寸、重量和打托数据吗?确认后将按实测数据入库。', '确认实测数据', { confirmButtonText: '确认入库', cancelButtonText: '取消' }) } catch { return }
try { selectedDetail.value = await confirmTransferMeasurement(transferNo) as Record<string, string>; ElMessage.success('实测数据已确认并入库') }
catch (error) { ElMessage.error(error instanceof Error ? error.message : '确认失败') }
}
async function printTransferCartons() {
if (props.service !== 'transfer') return
try {
const detail = selectedDetail.value
const transferNo = String(detail?.transferNo || detail?.['0'] || '')
const label = await getTransferCartonLabel(transferNo)
if (!label.cartons.length) { ElMessage.warning('暂无可打印的箱唛'); return }
const printWindow = window.open('', '_blank')
if (!printWindow) { ElMessage.warning('浏览器阻止了打印窗口,请允许弹窗后重试'); return }
printWindow.opener = null; printWindow.document.open(); printWindow.document.write(label.printableHtml); printWindow.document.close(); printWindow.focus(); printWindow.print()
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '箱唛加载失败') }
}
2026-09-10 15:34:03 +08:00
async function confirmRelabel() {
const relabelNo = selectedRelabelDetail.value?.application.relabelNo
if (!relabelNo) return
try {
await ElMessageBox.confirm('确认仓库上传的换标结果无误并继续出库流程吗?', '换标完成确认', { confirmButtonText: '确认并继续', cancelButtonText: '取消' })
selectedRelabelDetail.value = await confirmRelabelCompletion(relabelNo)
ElMessage.success('已确认换标完成,订单已进入后续出库流程')
} catch (error) { if (error !== 'cancel') ElMessage.error(error instanceof Error ? error.message : '确认失败') }
}
async function previewCompletionPhoto(fileId: string) {
const relabelNo = selectedRelabelDetail.value?.application.relabelNo
if (!relabelNo) return
try {
const blob = await downloadRelabelCompletionPhoto(relabelNo, fileId)
const url = URL.createObjectURL(blob)
window.open(url, '_blank', 'noopener,noreferrer')
window.setTimeout(() => URL.revokeObjectURL(url), 60_000)
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '完工照片加载失败') }
}
2026-09-08 20:28:05 +08:00
async function loadWarehouses() {
fbaWarehousesLoading.value = true
fbaWarehousesError.value = ''
2026-09-13 21:10:14 +08:00
operationWarehousesError.value = ''
2026-09-08 20:28:05 +08:00
try {
2026-09-13 21:10:14 +08:00
const [fbaResult, operationResult] = await Promise.allSettled([
2026-09-08 20:28:05 +08:00
getMerchantWarehouses({ warehouseType: 'FBA' }),
getMerchantWarehouses({ warehouseType: 'OVERSEAS' }),
])
2026-09-13 21:10:14 +08:00
fbaWarehouses.value = fbaResult.status === 'fulfilled' ? fbaResult.value.records : []
operationWarehouses.value = operationResult.status === 'fulfilled' ? operationResult.value.records : []
if (operationResult.status === 'rejected') operationWarehousesError.value = '海外仓候选加载失败,请检查仓库主数据或数据库迁移'
2026-09-08 20:28:05 +08:00
} catch (error) {
fbaWarehouses.value = []
fbaWarehousesError.value = error instanceof Error ? error.message : 'FBA 目的仓加载失败'
ElMessage.error(fbaWarehousesError.value)
} finally { fbaWarehousesLoading.value = false }
}
function warehouseLabel(value: MerchantWarehouse) { return `${value.warehouseName} · ${value.warehouseCode}` }
2026-09-10 15:34:03 +08:00
async function selectTransferWarehouse() {
selectedOperationWarehouse.value = null
form.contact = ''
form.phone = ''
2026-09-13 21:10:14 +08:00
if (form.deliveryMethod === 'CUSTOMER_DELIVERY') form.address = ''
2026-09-10 15:34:03 +08:00
if (!form.warehouse) return
operationWarehouseDetailLoading.value = true
try {
const detail = await getMerchantWarehouseDetail(form.warehouse)
selectedOperationWarehouse.value = detail
form.contact = detail.warehouse.contactName || ''
form.phone = detail.warehouse.contactPhone || ''
if (form.deliveryMethod === 'CUSTOMER_DELIVERY') form.address = operationWarehouseAddress.value
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '中转仓联系信息加载失败')
} finally { operationWarehouseDetailLoading.value = false }
}
function selectTransferInboundMethod(value: string | number | boolean | undefined) {
form.address = value === 'CUSTOMER_DELIVERY' ? operationWarehouseAddress.value : ''
}
function selectTransferFbaAvailability(value: string | number | boolean | undefined) {
if (value === true) return
form.destination = ''
form.fbaShipmentNo = ''
}
2026-09-08 20:28:05 +08:00
async function selectOperationWarehouse() {
cargoRows.value = []
2026-09-13 21:10:14 +08:00
transferRelabelRows.value = []
2026-09-08 20:28:05 +08:00
skuRelabelRows.value = []
2026-09-10 17:12:52 +08:00
importedRelabelBindings.value = []
2026-09-08 20:28:05 +08:00
selectedRelabelInboundOrderId.value = ''
relabelCandidates.value = []
const warehouse = operationWarehouses.value.find((item) => String(item.id) === form.warehouse)
form.contact = warehouse?.contactName || ''
form.phone = warehouse?.contactPhone || ''
if (!form.warehouse) return
relabelCandidatesLoading.value = true
try { relabelCandidates.value = await listRelabelInventoryCandidates(form.warehouse) }
catch (error) { ElMessage.error(error instanceof Error ? error.message : '当前客户库存加载失败') }
finally { relabelCandidatesLoading.value = false }
}
function selectRelabelInventory(inboundOrderId: string) {
cargoRows.value = relabelCandidates.value
.filter((candidate) => String(candidate.inboundOrderId) === inboundOrderId)
2026-09-10 15:34:03 +08:00
.flatMap((candidate) => candidate.positions.flatMap((position) => Array.from({ length: Math.min(position.availablePackages, 500) }, () => ({
id: ++cargoSequence, sku: candidate.skuCode || '', name: candidate.productNameZh || '库存货物',
2026-09-10 17:12:52 +08:00
included: false, quantity: 1, availableQuantity: 1,
2026-09-08 20:28:05 +08:00
sourceInventoryBalanceId: String(candidate.inventoryBalanceId), sourceInventoryPositionId: String(position.positionId),
sourceInboundOrderId: String(candidate.inboundOrderId), sourceInboundOrderNo: candidate.inboundOrderNo,
sourceInboundCode: candidate.inboundCode, sourceWarehouseName: candidate.overseasWarehouseName,
palletNo: position.palletNo, warehouseLocationCode: position.warehouseLocationCode,
2026-09-13 21:10:14 +08:00
fnsku: '', shipmentNo: '', newSkuCode: '', shippingMark: '', customerCartonMarks: '', cartonMark: '', fbaCartonMark: '',
2026-09-10 15:34:03 +08:00
destinationWarehouseId: candidate.destinationFbaWarehouseCode || '', remark: '',
})))).slice(0, 500)
2026-09-10 17:12:52 +08:00
if (cargoRows.value.length) cargoRows.value[0].included = true
2026-09-08 20:28:05 +08:00
skuRelabelRows.value = []
2026-09-10 17:12:52 +08:00
importedRelabelBindings.value = []
2026-09-10 15:34:03 +08:00
shippingLabelFile.value = null
2026-09-09 13:35:51 +08:00
if (labelingMode.value !== 'carton') addSkuRelabelRow()
2026-09-08 20:28:05 +08:00
}
2026-08-21 17:20:51 +08:00
</script>
<template>
<section class="warehouse-service-page">
<template v-if="mode === 'list'">
<div class="warehouse-breadcrumb"><span>首页</span><b>/</b><strong>{{ config.title }}</strong></div>
<div class="warehouse-page-heading"><h2>{{ config.title }}</h2><el-button type="primary" @click="openCreate"><Plus :size="16" />{{ props.service === 'relabel' ? '新建退货换标单' : props.service === 'transfer' ? '新增订单' : `新增${config.title}` }}</el-button></div>
<div class="warehouse-card warehouse-filter-card">
<div class="warehouse-filter-grid">
<label><span>{{ config.numberLabel }}</span><el-input v-model="filters.number" :placeholder="`请输入${config.numberLabel}`" clearable /></label>
<label v-if="props.service !== 'labeling' && props.service !== 'relabel'"><span>进仓编号</span><el-input v-model="filters.inboundNo" placeholder="请输入进仓编号" clearable /></label>
<label><span>{{ props.service === 'relabel' ? '来源 FBA 仓' : '操作仓' }}</span><el-select v-model="filters.warehouse" placeholder="全部"><el-option label="美西洛杉矶仓" value="LAX" /><el-option label="美东新泽西仓" value="EWR" /><el-option label="美中芝加哥仓" value="ORD" /></el-select></label>
<label v-if="props.service === 'transfer' || props.service === 'relabel'"><span>目的 FBA 仓</span><el-input v-model="filters.destination" placeholder="请输入仓代码" clearable /></label>
<label><span>订单状态</span><el-select v-model="filters.status" placeholder="全部状态" clearable><el-option v-for="option in statusOptions" :key="option.value" :label="option.label" :value="option.value" /></el-select></label>
<label><span>创建时间</span><el-date-picker v-model="filters.start" type="date" placeholder="开始日期" value-format="YYYY-MM-DD" /></label>
<label><span>&nbsp;</span><el-date-picker v-model="filters.end" type="date" placeholder="结束日期" value-format="YYYY-MM-DD" /></label>
</div>
<div class="warehouse-filter-actions"><el-button type="primary" @click="query"><Search :size="16" />查询</el-button><el-button @click="resetFilters(); query()">重置</el-button></div>
</div>
<div v-loading="loading" class="warehouse-card warehouse-table-card">
2026-09-07 19:15:18 +08:00
<div class="warehouse-table-scroll"><table><thead><tr><th v-for="column in config.columns" :key="column">{{ column }}</th></tr></thead><tbody><tr v-for="(row, rowIndex) in submittedRows" :key="rowIndex"><td><button class="warehouse-link" type="button" @click="openDetail(row)"><Eye :size="15" />查看</button></td><td v-for="(_, index) in config.columns.slice(1)" :key="index"><SingleLineText :value="row[String(index)]" /></td></tr><tr v-if="submittedRows.length === 0"><td :colspan="config.columns.length"><div class="warehouse-empty">{{ config.emptyText }}</div></td></tr></tbody></table></div>
2026-08-21 17:20:51 +08:00
<div class="warehouse-pagination"><span>共 <strong>{{ submittedRows.length }}</strong> 条</span><el-button size="small" disabled>上一页</el-button><button type="button" class="warehouse-page-number">1</button><el-button size="small" disabled>下一页</el-button></div>
</div>
</template>
<template v-else-if="mode === 'create'">
<div class="warehouse-breadcrumb"><button type="button" @click="mode = 'list'">{{ config.title }}</button><b>/</b><strong>新增</strong></div>
<div class="warehouse-page-heading"><h2>{{ config.createTitle }}</h2><el-button @click="mode = 'list'"><ArrowLeft :size="16" />返回列表</el-button></div>
2026-09-13 21:10:14 +08:00
<form :class="{ 'warehouse-transfer-form': props.service === 'transfer', 'warehouse-transfer-form--relabel': props.service === 'transfer' && form.relabel }" @submit.prevent="submit">
<div class="warehouse-card warehouse-form-section warehouse-transfer-main-card"><h3>{{ props.service === 'transfer' ? '入库与转运信息' : props.service === 'relabel' ? '退货信息' : props.service === 'labeling' ? '订单信息' : '基础信息' }}</h3>
2026-08-21 17:20:51 +08:00
<div class="warehouse-form-grid">
<label v-if="props.service === 'disposal'" class="warehouse-span-2"><span><i>*</i>选择入库单</span><el-select v-model="selectedDiscardOrderId" filterable :loading="discardCandidatesLoading" placeholder="请选择有可用库存的入库单" @change="selectDiscardInbound"><el-option v-for="candidate in discardCandidates" :key="candidate.orderId" :label="`${candidate.inboundOrderNo || candidate.inboundCode || '—'}|${candidate.inboundCode || '—'}|${candidate.overseasWarehouseName || '—'}|可用 ${candidate.availablePackages} 箱`" :value="candidate.orderId" /></el-select><small class="warehouse-hint">仅显示一件代发中仍有可用箱数的入库单。</small></label>
<label v-if="props.service === 'relabel'"><span><i>*</i>进仓编号</span><el-input v-model="form.inboundNo" placeholder="请输入已有入仓编号" /></label>
2026-09-08 20:28:05 +08:00
<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>
2026-08-21 17:20:51 +08:00
<label v-else-if="props.service === 'disposal'"><span>进仓编号</span><el-input v-model="form.inboundNo" readonly /></label>
2026-09-13 21:10:14 +08:00
<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><small v-else-if="props.service === 'labeling'" class="warehouse-hint">选择后仅查询当前客户在该中转仓的可用库存。</small><small v-else-if="props.service === 'transfer'" class="warehouse-hint">本批货物将先进入该仓完成签收和入库。</small></label>
2026-09-08 20:28:05 +08:00
<label v-if="props.service === 'labeling'" class="warehouse-span-2"><span><i>*</i>选择有库存的历史入仓单</span><el-select v-model="selectedRelabelInboundOrderId" filterable clearable :loading="relabelCandidatesLoading" :disabled="!form.warehouse" no-data-text="当前中转仓暂无已入库可用库存" placeholder="请选择曾经下单且当前仍有库存的入仓单" @change="selectRelabelInventory"><el-option v-for="option in relabelInboundOptions" :key="option.value" :label="`${option.inboundOrderNo}|入仓码 ${option.inboundCode}|可用 ${option.availablePackages} 箱`" :value="option.value" /></el-select><small class="warehouse-hint">仅显示当前客户在所选中转仓已经完成入库且仍有可用库存的历史入仓单。</small></label>
2026-08-21 17:20:51 +08:00
<label v-if="props.service === 'disposal'"><span>操作仓</span><el-input :model-value="discardCandidates.find((item) => item.orderId === selectedDiscardOrderId)?.overseasWarehouseName || form.warehouse" readonly /></label>
2026-09-10 15:34:03 +08:00
<div v-if="props.service === 'transfer'" class="warehouse-choice-field warehouse-span-2"><span><i>*</i>货物如何送到中转仓</span><el-radio-group v-model="form.deliveryMethod" @change="selectTransferInboundMethod"><el-radio-button value="WAREHOUSE_PICKUP">仓库上门提货</el-radio-button><el-radio-button value="CUSTOMER_DELIVERY">客户送货到仓</el-radio-button></el-radio-group><small>仓库上门提货:填写货物当前地址;客户送货到仓:下方自动展示中转仓收货信息。</small></div>
<label v-if="props.service === 'transfer' && form.deliveryMethod === 'WAREHOUSE_PICKUP'" class="warehouse-span-2"><span><i>*</i>提货地址</span><el-input v-model="form.address" placeholder="请输入仓库上门提货的详细地址" /></label>
<div v-if="props.service === 'transfer' && form.deliveryMethod === 'CUSTOMER_DELIVERY'" v-loading="operationWarehouseDetailLoading" class="warehouse-receiving-card warehouse-span-2">
2026-09-13 21:10:14 +08:00
<div><span>仓库收货信息</span><strong>{{ operationWarehouseAddress || '该中转仓暂未维护收货地址' }}</strong></div>
<label class="warehouse-span-2"><span><i>*</i>送货地址</span><el-input v-model="form.address" placeholder="请输入实际送货地址" /></label>
2026-09-10 15:34:03 +08:00
<div><span>收货联系人</span><strong>{{ form.contact || '未维护' }}</strong></div>
<div><span>联系电话</span><strong>{{ form.phone || '未维护' }}</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>
2026-08-21 17:20:51 +08:00
<label v-if="props.service === 'relabel' || props.service === 'disposal'"><span><i>*</i>{{ props.service === 'relabel' ? '退货原因' : '废弃原因' }}</span><el-select v-model="form.reason" placeholder="请选择"><el-option label="客户退货" value="客户退货" /><el-option label="亚马逊移除" value="亚马逊移除" /><el-option label="残损不可售" value="残损不可售" /></el-select></label>
2026-09-08 20:28:05 +08:00
<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>
2026-08-21 17:20:51 +08:00
<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>
2026-09-08 20:28:05 +08:00
<label v-if="props.service === 'labeling'"><span><i>*</i>贴标费用(CNY)</span><el-input-number v-model="form.chargeAmount" :min="0" :precision="2" controls-position="right" placeholder="请输入贴标费用" /></label>
2026-09-10 15:34:03 +08:00
<label v-if="props.service === 'labeling' && labelingMode !== 'sku'"><span><i>*</i>换标后派送方式</span><el-select v-model="form.deliveryMethod" placeholder="请选择派送方式"><el-option label="卡车派送" value="TRUCK" /><el-option label="快递派送" value="EXPRESS" /><el-option label="卡车自提" value="TRUCK_PICKUP" /><el-option label="快递自提" value="EXPRESS_PICKUP" /></el-select><small class="warehouse-hint">卡车派送进入预排车;其余方式确认换标完成后进入换标出库。</small></label>
<div v-if="props.service === 'labeling' && labelingMode !== 'sku' && (form.deliveryMethod === 'EXPRESS' || form.deliveryMethod === 'EXPRESS_PICKUP')" class="warehouse-checkbox-field"><span>是否打印面单</span><el-checkbox v-model="form.relabel">需要仓库打印面单</el-checkbox></div>
<label v-if="props.service === 'labeling' && labelingMode !== 'sku' && (form.deliveryMethod === 'EXPRESS' || form.deliveryMethod === 'EXPRESS_PICKUP') && form.relabel"><span><i>*</i>上传面单文件</span><input class="warehouse-native-file" type="file" accept="application/pdf,image/*,.pdf" @change="selectShippingLabel"><small class="warehouse-hint">支持 PDF 或图片,最大 20 MB。</small></label>
<label v-if="props.service === 'relabel'" class="warehouse-span-2"><span><i>*</i>详细派送地址</span><el-input v-model="form.address" placeholder="请输入派送地址" /></label>
2026-09-13 21:10:14 +08:00
<label v-if="props.service === 'transfer' || props.service === 'relabel'"><span><i>*</i>预估总体积(CBM)</span><el-input-number v-model="form.volumeCbm" :min="0" :precision="4" :step="0.0001" controls-position="right" placeholder="请输入预估总体积" /></label>
2026-09-10 15:34:03 +08:00
<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>
2026-09-13 21:10:14 +08:00
<label v-if="props.service === 'transfer'"><span><i>*</i>预计发货时间</span><el-date-picker v-model="form.estimatedShipAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择预计发货时间" /></label>
2026-09-10 15:34:03 +08:00
<div v-if="props.service === 'transfer'" class="warehouse-choice-field warehouse-span-2"><span><i>*</i>是否已有 FBA 去向</span><el-radio-group v-model="transferHasFbaDestination" @change="selectTransferFbaAvailability"><el-radio-button :value="false">暂未确定</el-radio-button><el-radio-button :value="true">已有 FBA 去向</el-radio-button></el-radio-group><small>暂未确定也可以提交,货物先正常入中转仓,后续再补充转运指令。</small></div>
2026-09-13 21:10:14 +08:00
<label v-if="props.service === 'transfer' && transferHasFbaDestination && !form.relabel"><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' && transferHasFbaDestination && !form.relabel"><span><i>*</i>FBA 货件编号</span><el-input v-model="form.fbaShipmentNo" placeholder="请输入 FBA 货件编号" /></label>
2026-09-10 15:34:03 +08:00
<label v-if="props.service === 'relabel'"><span><i>*</i>FBA 货件编号</span><el-input v-model="form.fbaShipmentNo" placeholder="请输入 FBA 货件编号" /></label>
2026-09-08 20:28:05 +08:00
<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>
2026-08-21 17:20:51 +08:00
<label class="warehouse-span-2"><span>订单备注</span><el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注信息" /></label>
2026-09-13 21:10:14 +08:00
<div v-if="props.service === 'transfer'" class="warehouse-choice-field warehouse-span-2"><span><i>*</i>是否需要换标</span><el-radio-group v-model="form.relabel"><el-radio-button :value="false">不需要换标</el-radio-button><el-radio-button :value="true">需要换标</el-radio-button></el-radio-group><small>{{ form.relabel ? '本次提交将同时登记换标申请,入库完成后自动进入换标作业。' : '入库完成且已有目的仓后,可直接进入排车流程。' }}</small></div>
2026-08-21 17:20:51 +08:00
</div>
</div>
2026-09-13 21:10:14 +08:00
<div class="warehouse-card warehouse-form-section warehouse-transfer-cargo-card">
2026-09-08 20:28:05 +08:00
<template v-if="props.service === 'labeling'">
2026-09-10 15:56:39 +08:00
<div class="warehouse-section-heading warehouse-labeling-heading">
<h3>货物明细</h3>
<div class="warehouse-template-actions">
2026-09-10 17:12:52 +08:00
<el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('sku')"><Download :size="15" />下载 SKU 关联模板</el-button>
<el-button size="small" :disabled="!cargoRows.length" @click="skuTemplateInput?.click()"><Upload :size="15" />上传 SKU 关联</el-button>
<el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('carton')"><Download :size="15" />下载箱号关联模板</el-button>
<el-button size="small" :disabled="!cargoRows.length" @click="cartonTemplateInput?.click()"><Upload :size="15" />上传箱号关联</el-button>
<el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('mixed')"><Download :size="15" />下载 SKU 混箱模板</el-button>
<el-button size="small" :disabled="!cargoRows.length" @click="mixedTemplateInput?.click()"><Upload :size="15" />上传 SKU 混箱</el-button>
<input ref="skuTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'sku')">
<input ref="cartonTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'carton')">
<input ref="mixedTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'mixed')">
2026-09-10 15:56:39 +08:00
</div>
</div>
2026-09-09 13:35:51 +08:00
<div class="warehouse-labeling-mode">
<span>换标类型</span>
<el-radio-group v-model="labelingMode" aria-label="换标类型">
<el-radio-button value="carton">只换箱唛</el-radio-button>
<el-radio-button value="sku">只换 SKU 标签</el-radio-button>
<el-radio-button value="carton_and_sku">SKU 和箱唛都换</el-radio-button>
</el-radio-group>
<p>{{ labelingModeHint }}</p>
</div>
2026-09-10 17:12:52 +08:00
<div v-if="importedRelabelBindings.length" class="warehouse-import-preview">
<div class="warehouse-section-heading"><div><strong>待导入绑定关系</strong><p>可只勾选本次需要换标的部分,也可以全选后一次导入。</p></div><div><el-checkbox :model-value="importedRelabelBindings.every((item) => item.selected)" @change="selectAllImportedBindings(Boolean($event))">全选</el-checkbox><el-button type="primary" size="small" @click="importSelectedBindingsToDetails">导入到明细</el-button></div></div>
<div class="warehouse-table-scroll"><table class="warehouse-binding-preview-table"><thead><tr><th>选择</th><th>关系类型</th><th>旧箱唛</th><th>旧 SKU</th><th>新 SKU</th><th>数量</th><th>新 FBA 箱唛</th><th>FBA 目的仓</th></tr></thead><tbody><tr v-for="item in importedRelabelBindings" :key="item.id"><td><el-checkbox v-model="item.selected" /></td><td>{{ item.templateType === 'mixed' ? 'SKU 混箱换标' : item.templateType === 'carton' ? '箱号关联' : 'SKU 关联' }}</td><td>{{ item.cartonMark }}</td><td>{{ item.originalSku || '—' }}</td><td>{{ item.newSku || '—' }}</td><td>{{ item.quantity }}</td><td>{{ item.fbaCartonMark || '—' }}</td><td>{{ warehouseDisplay(item.destinationWarehouseId) }}</td></tr></tbody></table></div>
</div>
2026-09-09 13:35:51 +08:00
<div class="warehouse-section-heading warehouse-tab-actions">
2026-09-10 17:12:52 +08:00
<div><strong>绑定明细</strong><p>上传后在下方回显,也可以手动添加;SKU 与箱号关联通过旧箱唛合并。系统只提供大概库存位置,不代表箱唛已绑定具体托盘。</p></div>
2026-09-10 15:56:39 +08:00
<div v-if="labelingMode === 'carton'"><el-button type="primary" size="small" @click="addRelabelCargoRow"><Plus :size="15" />手动添加行</el-button><el-button size="small" @click="clearRelabelRows">清空</el-button></div>
2026-09-09 13:35:51 +08:00
<div v-else><el-button type="primary" size="small" @click="addSkuRelabelRow"><Plus :size="15" />添加 SKU 明细</el-button><el-button size="small" @click="skuRelabelRows = []">清空</el-button></div>
</div>
2026-09-10 17:12:52 +08:00
<div v-if="labelingMode === 'carton'" class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-label-table"><thead><tr><th>商品 SKU</th><th>中文品名 *</th><th>FBA 货件编号 *</th><th>箱数</th><th>旧箱唛 *</th><th>新 FBA 箱唛 *</th><th>FBA 目的仓 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in cartonDetailRows" :key="row.id"><td><el-input v-model="row.sku" placeholder="选填" /></td><td><el-input v-model="row.name" placeholder="请输入中文品名" /></td><td><el-input v-model="row.shipmentNo" placeholder="请输入FBA货件编号" /></td><td><strong>1 箱</strong></td><td><el-input v-model="row.cartonMark" placeholder="请输入旧箱唛" /></td><td><el-input v-model="row.fbaCartonMark" placeholder="请输入新的 FBA 箱唛" /></td><td><el-select v-model="row.destinationWarehouseId" :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></td><td><el-input v-model="row.remark" type="textarea" :rows="2" maxlength="1000" show-word-limit placeholder="请输入明细备注" /></td><td><button type="button" class="warehouse-delete" title="移出本次明细" @click="removeCargoRow(row.id)"><Trash2 :size="16" />移出</button></td></tr><tr v-if="cartonDetailRows.length === 0"><td colspan="9"><div class="warehouse-empty">暂无本次换标明细,请上传关系后导入或手动添加</div></td></tr></tbody></table></div>
<div v-else class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-sku-label-table"><thead><tr><th>旧箱唛 *</th><th>旧 SKU *</th><th>新 SKU *</th><th>换标数量(件)*</th><th v-if="labelingMode === 'carton_and_sku'">新 FBA 箱唛 *</th><th v-if="labelingMode === 'carton_and_sku'">FBA 目的仓 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in skuRelabelRows" :key="row.id"><td><el-input v-model="row.cartonMark" placeholder="请输入旧箱唛" @change="applyExistingCartonBinding(row)" /></td><td><el-input v-model="row.originalSku" placeholder="请输入旧 SKU" @change="applyExistingSkuBinding(row)" /></td><td><el-input v-model="row.newSku" placeholder="请输入新 SKU" /></td><td><el-input-number v-model="row.quantity" :min="1" :precision="0" controls-position="right" placeholder="请输入件数" /></td><td v-if="labelingMode === 'carton_and_sku'"><el-input v-model="row.fbaCartonMark" placeholder="请输入新的 FBA 箱唛" /></td><td v-if="labelingMode === 'carton_and_sku'"><el-select v-model="row.destinationWarehouseId" :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></td><td><el-input v-model="row.remark" placeholder="请输入备注" /></td><td><button type="button" class="warehouse-delete" title="删除" @click="removeSkuRelabelRow(row.id)"><Trash2 :size="16" />删除</button></td></tr><tr v-if="skuRelabelRows.length === 0"><td :colspan="labelingMode === 'carton_and_sku' ? 8 : 6"><div class="warehouse-empty">暂无绑定明细,请点击“添加 SKU 明细”</div></td></tr></tbody></table></div>
2026-09-08 20:28:05 +08:00
</template>
<template v-else>
<div class="warehouse-section-heading"><h3>货物明细</h3><div v-if="props.service !== 'disposal'"><el-button type="primary" size="small" @click="addCargoRow"><Plus :size="15" />添加行</el-button><el-button size="small" @click="cargoRows = []">清空</el-button></div></div>
<div class="warehouse-table-scroll"><table class="warehouse-edit-table"><thead><tr><th>商品 SKU *</th><th>中文品名 *</th><th v-if="props.service === 'disposal'">可用箱数</th><th v-if="props.service === 'relabel'">FNSKU / 箱唛</th><th>{{ props.service === 'disposal' ? '本次废弃箱数' : '预计箱数' }} *</th><th v-if="props.service !== 'disposal'">操作</th></tr></thead><tbody><tr v-for="row in cargoRows" :key="row.id"><td><el-input v-model="row.sku" :readonly="props.service === 'disposal'" placeholder="请输入商品 SKU" /></td><td><el-input v-model="row.name" :readonly="props.service === 'disposal'" placeholder="请输入中文品名" /></td><td v-if="props.service === 'disposal'"><strong>{{ row.availableQuantity }} 箱</strong></td><td v-if="props.service === 'relabel'"><el-input v-model="row.shippingMark" placeholder="请输入FNSKU或箱唛" /></td><td><el-input-number v-model="row.quantity" :min="props.service === 'disposal' ? 0 : 1" :max="row.availableQuantity" controls-position="right" placeholder="请输入箱数" /></td><td v-if="props.service !== 'disposal'"><button type="button" class="warehouse-delete" title="删除" @click="removeCargoRow(row.id)"><Trash2 :size="16" />删除</button></td></tr><tr v-if="cargoRows.length === 0"><td :colspan="props.service === 'disposal' ? 4 : props.service === 'relabel' ? 5 : 4"><div class="warehouse-empty">{{ props.service === 'disposal' ? '请先选择入库单,系统将自动带出 SKU 和可用箱数' : '暂无货物明细,请点击“添加行”' }}</div></td></tr></tbody></table></div>
2026-09-08 20:28:05 +08:00
</template>
2026-08-21 17:20:51 +08:00
</div>
2026-09-13 21:10:14 +08:00
<div v-if="props.service === 'transfer' && form.relabel" class="warehouse-card warehouse-form-section warehouse-transfer-relabel">
<div class="warehouse-section-heading warehouse-labeling-heading"><div><h3>货物明细</h3><p>换标明细与贴标申请保持一致;客户只填写自己的箱唛,我方箱唛由系统生成。</p></div><div class="warehouse-template-actions"><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('sku')"><Download :size="15" />下载 SKU 关联模板</el-button><el-button size="small" :disabled="!cargoRows.length" @click="skuTemplateInput?.click()"><Upload :size="15" />上传 SKU 关联</el-button><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('carton')"><Download :size="15" />下载箱号关联模板</el-button><el-button size="small" :disabled="!cargoRows.length" @click="cartonTemplateInput?.click()"><Upload :size="15" />上传箱号关联</el-button><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('mixed')"><Download :size="15" />下载 SKU 混箱模板</el-button><el-button size="small" :disabled="!cargoRows.length" @click="mixedTemplateInput?.click()"><Upload :size="15" />上传 SKU 混箱</el-button><input ref="skuTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'sku')"><input ref="cartonTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'carton')"><input ref="mixedTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'mixed')"></div></div>
<div class="warehouse-form-grid warehouse-transfer-relabel-fields"><label><span><i>*</i>换标截止时间</span><el-date-picker v-model="form.labelDeadline" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择截止时间" /></label><label><span><i>*</i>联系人</span><el-input v-model="form.contact" placeholder="请输入联系人" /></label><label><span><i>*</i>联系电话</span><el-input v-model="form.phone" placeholder="请输入联系电话" /></label><label><span><i>*</i>换标费用(CNY)</span><el-input-number v-model="form.chargeAmount" :min="0.01" :precision="2" :step="0.01" controls-position="right" /></label><label v-if="labelingMode !== 'sku'"><span><i>*</i>换标后派送方式</span><el-select v-model="form.relabelDeliveryMethod" placeholder="请选择派送方式"><el-option label="卡车派送" value="TRUCK" /><el-option label="快递派送" value="EXPRESS" /><el-option label="卡车自提" value="TRUCK_PICKUP" /><el-option label="快递自提" value="EXPRESS_PICKUP" /></el-select></label><label><span>FBA 货件编号</span><el-input v-model="form.fbaShipmentNo" placeholder="选填,未填则使用转运单号" /></label><label class="warehouse-span-2"><span>贴标要求</span><el-input v-model="form.requirement" type="textarea" :rows="2" maxlength="1000" show-word-limit placeholder="请输入贴标要求(选填)" /></label></div>
<div class="warehouse-labeling-mode"><span>换标类型</span><el-radio-group v-model="labelingMode" aria-label="换标类型"><el-radio-button value="carton">只换箱唛</el-radio-button><el-radio-button value="sku">只换 SKU 标签</el-radio-button><el-radio-button value="carton_and_sku">SKU 和箱唛都换</el-radio-button></el-radio-group><p>{{ labelingModeHint }}</p></div>
<div v-if="importedRelabelBindings.length" class="warehouse-import-preview"><div class="warehouse-section-heading"><div><strong>待导入绑定关系</strong><p>可只勾选本次需要换标的部分,也可以全选后一次导入。</p></div><div><el-checkbox :model-value="importedRelabelBindings.every((item) => item.selected)" @change="selectAllImportedBindings(Boolean($event))">全选</el-checkbox><el-button type="primary" size="small" @click="importSelectedBindingsToDetails">导入到明细</el-button></div></div><div class="warehouse-table-scroll"><table class="warehouse-binding-preview-table"><thead><tr><th>选择</th><th>关系类型</th><th>旧箱唛</th><th>旧 SKU</th><th>新 SKU</th><th>数量</th><th>新 FBA 箱唛</th><th>FBA 目的仓</th></tr></thead><tbody><tr v-for="item in importedRelabelBindings" :key="item.id"><td><el-checkbox v-model="item.selected" /></td><td>{{ item.templateType === 'mixed' ? 'SKU 混箱换标' : item.templateType === 'carton' ? '箱号关联' : 'SKU 关联' }}</td><td>{{ item.cartonMark }}</td><td>{{ item.originalSku || '—' }}</td><td>{{ item.newSku || '—' }}</td><td>{{ item.quantity }}</td><td>{{ item.fbaCartonMark || '—' }}</td><td>{{ warehouseDisplay(item.destinationWarehouseId) }}</td></tr></tbody></table></div></div>
<div class="warehouse-section-heading warehouse-tab-actions"><div><strong>绑定明细</strong><p>上传后在下方回显,也可以手动添加;客户箱唛作为旧箱唛来源,系统我方箱唛不需要手填。</p></div><div><el-button type="primary" size="small" @click="addTransferRelabelRow"><Plus :size="15" />添加 SKU 明细</el-button><el-button size="small" @click="clearTransferRelabelRows">清空</el-button></div></div>
<div v-if="labelingMode === 'carton'" class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-transfer-mark-table"><thead><tr><th>客户箱唛号 *</th><th>旧 SKU</th><th>新 FBA 箱唛 *</th><th>FBA 目的仓 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in transferRelabelRows" :key="row.id"><td><el-input v-model="row.customerCartonMarks" type="textarea" :rows="2" placeholder="请输入客户箱唛号" /></td><td><el-input :model-value="row.sku" readonly /></td><td><el-input v-model="row.fbaCartonMark" placeholder="请输入新的 FBA 箱唛" /></td><td><el-select v-model="row.destinationWarehouseId" :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></td><td><el-input v-model="row.remark" type="textarea" :rows="2" placeholder="请输入备注" /></td><td><button type="button" class="warehouse-delete" title="删除该明细" @click="transferRelabelRows = transferRelabelRows.filter((item) => item.id !== row.id)"><Trash2 :size="16" />删除</button></td></tr><tr v-if="transferRelabelRows.length === 0"><td colspan="6"><div class="warehouse-empty">暂无绑定明细,请点击“添加 SKU 明细”</div></td></tr></tbody></table></div>
<div v-else-if="labelingMode === 'sku'" class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-transfer-mark-table"><thead><tr><th>客户箱唛号 *</th><th>旧 SKU *</th><th>新 SKU *</th><th>换标数量(件)*</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in transferRelabelRows" :key="row.id"><td><el-input v-model="row.customerCartonMarks" type="textarea" :rows="2" placeholder="请输入客户箱唛号" /></td><td><el-input :model-value="row.sku" readonly /></td><td><el-input v-model="row.newSkuCode" placeholder="请输入新 SKU" /></td><td><el-input-number v-model="row.quantity" :min="1" :precision="0" controls-position="right" /></td><td><el-input v-model="row.remark" type="textarea" :rows="2" placeholder="请输入备注" /></td><td><button type="button" class="warehouse-delete" title="删除该明细" @click="transferRelabelRows = transferRelabelRows.filter((item) => item.id !== row.id)"><Trash2 :size="16" />删除</button></td></tr><tr v-if="transferRelabelRows.length === 0"><td colspan="6"><div class="warehouse-empty">暂无绑定明细,请点击“添加 SKU 明细”</div></td></tr></tbody></table></div>
<div v-else class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-transfer-mark-table"><thead><tr><th>客户箱唛号 *</th><th>旧 SKU *</th><th>新 SKU *</th><th>换标数量(件)*</th><th>新 FBA 箱唛 *</th><th>FBA 目的仓 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in transferRelabelRows" :key="row.id"><td><el-input v-model="row.customerCartonMarks" type="textarea" :rows="2" placeholder="请输入客户箱唛号" /></td><td><el-input :model-value="row.sku" readonly /></td><td><el-input v-model="row.newSkuCode" placeholder="请输入新 SKU" /></td><td><el-input-number v-model="row.quantity" :min="1" :precision="0" controls-position="right" /></td><td><el-input v-model="row.fbaCartonMark" placeholder="请输入新的 FBA 箱唛" /></td><td><el-select v-model="row.destinationWarehouseId" :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></td><td><el-input v-model="row.remark" type="textarea" :rows="2" placeholder="请输入备注" /></td><td><button type="button" class="warehouse-delete" title="删除该明细" @click="transferRelabelRows = transferRelabelRows.filter((item) => item.id !== row.id)"><Trash2 :size="16" />删除</button></td></tr><tr v-if="transferRelabelRows.length === 0"><td colspan="8"><div class="warehouse-empty">暂无绑定明细,请点击“添加 SKU 明细”</div></td></tr></tbody></table></div>
</div>
<div class="warehouse-submit-bar warehouse-transfer-submit-bar"><el-button @click="resetForm">重置</el-button><el-button type="primary" native-type="submit" :loading="saving">提交{{ config.title }}</el-button></div>
2026-08-21 17:20:51 +08:00
</form>
</template>
2026-09-06 18:11:42 +08:00
<template v-else>
<div class="warehouse-breadcrumb"><button type="button" @click="mode = 'list'">{{ config.title }}</button><b>/</b><strong>订单详情</strong></div>
2026-09-13 21:10:14 +08:00
<div class="warehouse-page-heading"><h2>{{ config.title }}详情</h2><div><el-button v-if="props.service === 'transfer' && selectedDetail?.status === 'WAIT_CUSTOMER_CONFIRMATION'" type="primary" @click="confirmTransferMeasurements">确认实测并入库</el-button><el-button @click="mode = 'list'"><ArrowLeft :size="16" />返回列表</el-button></div></div>
2026-09-08 20:28:05 +08:00
<div class="warehouse-card warehouse-form-section"><h3>订单概览</h3><div class="warehouse-detail-grid"><div v-for="(column, index) in config.columns.slice(1)" :key="column"><span>{{ column }}</span><strong>{{ selectedDetail?.[String(index)] || '—' }}</strong></div></div></div>
2026-09-13 21:10:14 +08:00
<div v-if="props.service === 'transfer'" class="warehouse-card warehouse-form-section warehouse-transfer-times"><h3>发货时间</h3><div class="warehouse-detail-grid"><div><span>预计发货时间</span><strong>{{ formatDateTime(selectedDetail?.estimatedShipAt) }}</strong></div><div><span>实际发货时间</span><strong>{{ formatDateTime(selectedDetail?.actualShipAt) }}</strong></div></div><p>实际发货时间由仓库出库或 PDA 完成后回填,客户不能手动修改。</p></div>
<div v-if="props.service === 'transfer'" class="warehouse-card warehouse-form-section">
<div class="warehouse-section-heading"><h3>箱唛明细</h3><el-button type="primary" @click="printTransferCartons"><Printer :size="16" />打印箱唛</el-button></div>
<div class="warehouse-table-scroll"><table><thead><tr><th>系统箱唛</th><th>客户箱唛</th><th>箱序</th><th>状态</th></tr></thead><tbody>
<tr v-for="carton in (selectedDetail?.cartons || [])" :key="carton.cartonMark"><td><strong>{{ carton.cartonMark }}</strong></td><td>{{ carton.customerCartonMark || '—' }}</td><td>{{ carton.cartonIndex }} / {{ carton.totalCartons }}</td><td>{{ carton.status }}</td></tr>
<tr v-if="!(selectedDetail?.cartons || []).length"><td colspan="4"><div class="warehouse-empty">暂无箱唛明细</div></td></tr>
</tbody></table></div>
</div>
2026-09-10 15:34:03 +08:00
<div v-if="props.service === 'labeling'" class="warehouse-card warehouse-form-section">
<div class="warehouse-section-heading"><h3>换标完成与出库</h3><el-button v-if="selectedRelabelDetail?.application.status === 'CUSTOMER_CONFIRM_PENDING'" type="primary" @click="confirmRelabel">换标完成确认</el-button></div>
<div class="warehouse-detail-grid warehouse-fulfillment-grid">
<div><span>派送方式</span><strong>{{ deliveryMethodLabel(selectedRelabelDetail?.application.deliveryMethod || '') || '无需出库' }}</strong></div>
<div><span>履约状态</span><strong>{{ selectedRelabelDetail?.application.fulfillmentStatus || '—' }}</strong></div>
<div><span>客户确认时间</span><strong>{{ formatDateTime(selectedRelabelDetail?.application.customerConfirmedAt) }}</strong></div>
</div>
<div class="warehouse-evidence"><span>仓库完工照片</span><div v-if="selectedRelabelDetail?.completionEvidence?.photoFileIds?.length" class="warehouse-photo-actions"><el-button v-for="(fileId, index) in selectedRelabelDetail.completionEvidence.photoFileIds" :key="fileId" size="small" plain @click="previewCompletionPhoto(fileId)"><Eye :size="14" />照片 {{ index + 1 }}</el-button></div><strong v-else>仓库尚未上传完工照片</strong></div>
<p v-if="selectedRelabelDetail?.completionEvidence?.completionRemark" class="warehouse-completion-remark">完工备注:{{ selectedRelabelDetail.completionEvidence.completionRemark }}</p>
</div>
2026-09-06 18:11:42 +08:00
<div v-if="props.service === 'labeling'" class="warehouse-card warehouse-form-section">
2026-09-08 20:28:05 +08:00
<h3>换标任务明细</h3>
<div class="warehouse-table-scroll"><table class="warehouse-relabel-detail-table"><thead><tr><th>换标模式</th><th>我方箱唛</th><th>新 FBA 箱唛</th><th>FBA 目的仓</th><th>任务箱数</th><th>商品 SKU</th><th>SKU 换标明细</th><th>FBA 货件编号</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 换标' : item.operationType === 'SKU_LABEL' ? 'SKU 换标' : '箱唛换标' }}</td><td><strong>{{ item.cartonMark || '—' }}</strong></td><td><strong>{{ item.fbaCartonMark || '—' }}</strong></td><td>{{ warehouseDisplay(item.destinationFbaWarehouseCode) }}</td><td>{{ item.totalPackageCount }} 箱</td><td>{{ item.skuCode || '—' }}</td><td><template v-if="item.skuItems?.length"><div v-for="skuItem in item.skuItems" :key="`${skuItem.skuCode}-${skuItem.newSkuCode}-${skuItem.quantity}`">{{ skuItem.skuCode }} → {{ skuItem.newSkuCode }} × {{ skuItem.quantity }} 件</div></template><span v-else>{{ item.newSkuCode || '—' }}</span></td><td>{{ item.shipmentNo || '—' }}</td><td>{{ item.labelRequirement || '—' }}</td><td>{{ item.remark || '—' }}</td></tr>
<tr v-if="!selectedRelabelDetail?.items?.length"><td colspan="10"><div class="warehouse-empty">暂无换标任务明细</div></td></tr>
2026-09-06 18:11:42 +08:00
</tbody></table></div>
</div>
</template>
2026-08-21 17:20:51 +08:00
</section>
</template>
<style scoped>
.warehouse-service-page{color:#25324a}.warehouse-breadcrumb{display:flex;align-items:center;gap:9px;margin:0 0 14px;color:#9aa3b2;font-size:12px}.warehouse-breadcrumb b{font-weight:400}.warehouse-breadcrumb strong{color:#606b7c}.warehouse-breadcrumb button{padding:0;border:0;background:transparent;color:#4376b8;cursor:pointer}.warehouse-page-heading{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}.warehouse-page-heading h2{margin:0;color:#17233a;font-size:22px;letter-spacing:0}.warehouse-card{margin-bottom:12px;border:1px solid #e7ebf1;border-radius:4px;background:#fff;box-shadow:0 1px 3px rgba(25,42,70,.04)}.warehouse-filter-card{padding:20px 22px 16px}.warehouse-filter-grid{display:grid;grid-template-columns:repeat(4,minmax(180px,1fr));gap:16px 20px}.warehouse-filter-grid label,.warehouse-form-grid label{display:flex;min-width:0;flex-direction:column;gap:7px}.warehouse-filter-grid label>span,.warehouse-form-grid label>span{color:#596579;font-size:12px}.warehouse-filter-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:18px}.warehouse-table-card{overflow:hidden}.warehouse-table-scroll{overflow-x:auto}table{width:100%;min-width:940px;border-collapse:collapse;font-size:12px}th{height:44px;padding:0 14px;border-bottom:1px solid #e8edf3;background:#f7f9fc;color:#4d5a6e;font-weight:650;text-align:left;white-space:nowrap}td{height:48px;padding:8px 14px;border-bottom:1px solid #eef1f5;color:#526176;white-space:nowrap}.warehouse-empty{display:grid;height:150px;color:#a0a9b7;place-items:center}.warehouse-link,.warehouse-delete{display:inline-flex;align-items:center;gap:5px;padding:4px;border:0;background:transparent;color:#2176d2;cursor:pointer}.warehouse-delete{color:#d64545}.warehouse-pagination{display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:14px 18px;color:#778398;font-size:12px}.warehouse-pagination span{margin-right:8px}.warehouse-page-number{width:28px;height:28px;border:1px solid #2176d2;border-radius:3px;background:#2176d2;color:#fff}.warehouse-form-section{padding:22px}.warehouse-form-section h3{margin:0 0 18px;padding-bottom:12px;border-bottom:1px solid #edf0f4;color:#26364f;font-size:15px;letter-spacing:0}.warehouse-form-grid{display:grid;grid-template-columns:repeat(2,minmax(240px,1fr));gap:18px 28px}.warehouse-form-grid i{margin-right:3px;color:#e24b4b;font-style:normal}.warehouse-span-2{grid-column:1/-1}.warehouse-section-heading{display:flex;align-items:center;justify-content:space-between}.warehouse-section-heading h3{flex:1}.warehouse-edit-table td{padding:9px 10px}.warehouse-edit-table :deep(.el-input-number){width:130px}.warehouse-hint{margin:12px 0 0;color:#8792a2;font-size:12px}.warehouse-submit-bar{display:flex;justify-content:center;gap:10px;padding:12px 0 30px}.warehouse-detail-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:24px}.warehouse-detail-grid div{display:flex;flex-direction:column;gap:7px}.warehouse-detail-grid span{color:#8a95a5;font-size:12px}.warehouse-detail-grid strong{color:#334157;font-size:13px}:deep(.el-button){border-radius:4px}:deep(.el-button span){display:inline-flex;align-items:center;gap:6px}:deep(.el-select),:deep(.el-date-editor.el-input){width:100%}@media(max-width:1100px){.warehouse-filter-grid{grid-template-columns:repeat(2,1fr)}.warehouse-detail-grid{grid-template-columns:repeat(2,1fr)}}
2026-09-09 13:35:51 +08:00
.warehouse-checkbox-field{display:flex;min-height:32px;align-items:center;justify-content:flex-start;gap:16px}.warehouse-checkbox-field>span{color:#596579;font-size:12px}.warehouse-checkbox-field :deep(.el-checkbox){margin-right:0}
2026-09-10 15:34:03 +08:00
.warehouse-choice-field{display:grid;grid-template-columns:150px auto 1fr;align-items:center;gap:14px;min-height:40px;padding:12px 14px;border:1px solid #e1e8f2;border-radius:5px;background:#f8faff}.warehouse-choice-field>span{color:#34445c;font-size:13px;font-weight:650}.warehouse-choice-field>span i{margin-right:3px;color:#e24b4b;font-style:normal}.warehouse-choice-field>small{color:#7a879a;font-size:12px}.warehouse-receiving-card{display:grid;grid-template-columns:2fr 1fr 1fr;gap:22px;padding:15px 16px;border:1px solid #cfe0f5;border-radius:5px;background:#f6faff}.warehouse-receiving-card div{display:flex;min-width:0;flex-direction:column;gap:6px}.warehouse-receiving-card span{color:#7c899c;font-size:12px}.warehouse-receiving-card strong{overflow-wrap:anywhere;color:#314561;font-size:13px;font-weight:600}
2026-09-08 20:28:05 +08:00
.warehouse-hidden-input{display:none}.warehouse-label-table{min-width:1120px}.warehouse-attachment-tip{margin:10px 0 12px;color:#7d8797;font-size:12px}.warehouse-mark-mapping{display:flex;flex-wrap:wrap;gap:8px;margin:4px 0 8px;padding:12px;border:1px solid #dce7f5;border-radius:5px;background:#f7faff}.warehouse-mark-mapping strong{width:100%;color:#34445c}.warehouse-mark-mapping span{padding:5px 9px;border-radius:3px;background:#eaf2fc;color:#3e6593;font-size:12px}
.warehouse-unmaintained{margin-left:8px;color:#e6a23c;font-size:12px;font-style:normal}
2026-09-10 15:56:39 +08:00
.warehouse-labeling-tabs{margin-top:-8px}.warehouse-labeling-heading{align-items:flex-start}.warehouse-labeling-heading h3{margin-bottom:16px}.warehouse-template-actions{display:flex;gap:8px;padding-bottom:12px}.warehouse-tab-actions{align-items:center;margin:2px 0 14px}.warehouse-tab-actions p{margin:0;color:#7d8797;font-size:12px}.warehouse-tab-actions>div{display:flex;gap:8px}.warehouse-sku-notice{margin:0 0 14px;padding:10px 12px;border:1px solid #f2d39b;border-radius:4px;background:#fff8e8;color:#8a6116;font-size:12px}.warehouse-sku-label-table{min-width:980px}.warehouse-sku-label-table :deep(.el-input-number){width:160px}:deep(.warehouse-labeling-tabs>.el-tabs__header){margin-bottom:18px}:deep(.warehouse-labeling-tabs .el-tabs__item){height:42px;padding:0 26px;font-weight:600}.warehouse-labeling-mode{display:flex;align-items:center;gap:14px;margin:0 0 16px;padding:14px 16px;border:1px solid #dce5f5;border-radius:5px;background:#f7f9ff}.warehouse-labeling-mode>span{color:#34445c;font-size:13px;font-weight:650;white-space:nowrap}.warehouse-labeling-mode p{margin:0;color:#71809a;font-size:12px}.warehouse-labeling-mode :deep(.el-radio-group){display:flex;flex-shrink:0}.warehouse-labeling-mode :deep(.el-radio-button__inner){padding:8px 14px}
2026-09-06 18:11:42 +08:00
.warehouse-relabel-detail-table{min-width:1420px}.warehouse-relabel-detail-table td strong{color:#253f67}
2026-09-10 15:34:03 +08:00
.warehouse-native-file{min-height:32px;padding:5px 8px;border:1px solid #dcdfe6;border-radius:4px;background:#fff;color:#606266}.warehouse-fulfillment-grid{margin-bottom:18px}.warehouse-evidence{display:flex;align-items:center;gap:16px;padding:14px;border:1px solid #e7ebf1;border-radius:4px;background:#f8fafc}.warehouse-evidence>span{color:#71809a;font-size:12px}.warehouse-evidence>strong{color:#8a95a5;font-size:12px;font-weight:400}.warehouse-photo-actions{display:flex;flex-wrap:wrap;gap:8px}.warehouse-completion-remark{margin:10px 0 0;color:#596579;font-size:12px}
2026-09-10 17:12:52 +08:00
.warehouse-template-actions{flex-wrap:wrap;justify-content:flex-end}.warehouse-import-preview{margin:0 0 16px;padding:14px;border:1px solid #cfe0f5;border-radius:5px;background:#f8fbff}.warehouse-import-preview .warehouse-section-heading{margin-bottom:10px}.warehouse-import-preview .warehouse-section-heading>div{display:flex;align-items:center;gap:10px}.warehouse-import-preview p{margin:0;color:#7d8797;font-size:12px}.warehouse-binding-preview-table{min-width:980px;background:#fff}
2026-09-13 21:10:14 +08:00
.warehouse-transfer-relabel .warehouse-labeling-heading>div:first-child{min-width:0}.warehouse-transfer-relabel .warehouse-labeling-heading h3{margin-bottom:6px}.warehouse-transfer-relabel .warehouse-labeling-heading p{margin:0 0 16px;color:#7d8797;font-size:12px}.warehouse-transfer-relabel-fields{margin-bottom:18px}.warehouse-transfer-mark-table{min-width:1420px}.warehouse-transfer-mark-table td{vertical-align:top}.warehouse-transfer-mark-table :deep(.el-textarea){min-width:220px}.warehouse-transfer-mark-table :deep(.el-input-number){width:150px}.warehouse-count-error{color:#d64545}
.warehouse-transfer-form{display:grid;grid-template-columns:minmax(0,1fr);gap:12px}.warehouse-transfer-form--relabel{grid-template-columns:minmax(0,1fr) minmax(0,1.35fr);align-items:start}.warehouse-transfer-form--relabel .warehouse-transfer-main-card{grid-column:1;grid-row:1}.warehouse-transfer-form--relabel .warehouse-transfer-cargo-card{grid-column:1;grid-row:2}.warehouse-transfer-form--relabel .warehouse-transfer-relabel{grid-column:2;grid-row:1 / span 2;margin-bottom:0}.warehouse-transfer-form--relabel .warehouse-transfer-submit-bar{grid-column:1 / -1}.warehouse-transfer-form :deep(.el-input__wrapper),.warehouse-transfer-form :deep(.el-textarea__inner),.warehouse-transfer-form :deep(.el-select__wrapper),.warehouse-transfer-form :deep(.el-date-editor.el-input__wrapper){min-height:32px}.warehouse-transfer-form .warehouse-form-section{padding:16px}.warehouse-transfer-form .warehouse-form-section h3{margin-bottom:12px;padding-bottom:9px;font-size:14px}.warehouse-transfer-form .warehouse-form-grid{gap:12px 16px}.warehouse-transfer-form .warehouse-choice-field{min-height:34px;padding:8px 10px;grid-template-columns:118px auto 1fr;gap:10px}.warehouse-transfer-form .warehouse-choice-field>span{font-size:12px}.warehouse-transfer-form .warehouse-choice-field small{font-size:11px}.warehouse-transfer-form .warehouse-receiving-card{gap:12px;padding:10px 12px}.warehouse-transfer-form .warehouse-section-heading{margin-bottom:8px}.warehouse-transfer-form .warehouse-edit-table{font-size:11px}.warehouse-transfer-form .warehouse-edit-table th{height:36px;padding:0 9px}.warehouse-transfer-form .warehouse-edit-table td{height:42px;padding:6px 7px}.warehouse-transfer-form .warehouse-transfer-mark-table :deep(.el-textarea){min-width:150px}.warehouse-transfer-form .warehouse-transfer-mark-table :deep(.el-input-number){width:112px}.warehouse-transfer-form .warehouse-hint{margin-top:5px;font-size:11px}.warehouse-transfer-form .warehouse-labeling-mode{gap:9px;margin-bottom:10px;padding:9px 10px}.warehouse-transfer-form .warehouse-labeling-mode>span{font-size:12px}.warehouse-transfer-form .warehouse-labeling-mode p{font-size:11px}.warehouse-transfer-form .warehouse-labeling-mode :deep(.el-radio-button__inner){padding:6px 9px}.warehouse-transfer-form .warehouse-template-actions{gap:5px;padding-bottom:8px}.warehouse-transfer-form .warehouse-template-actions :deep(.el-button){padding:6px 8px;font-size:11px}.warehouse-transfer-form .warehouse-submit-bar{padding:4px 0 18px}.warehouse-transfer-form .warehouse-submit-bar :deep(.el-button){min-height:32px;padding:7px 14px}.warehouse-transfer-form .warehouse-transfer-relabel{overflow:hidden}.warehouse-transfer-form .warehouse-transfer-relabel-fields{margin-bottom:10px}
@media(max-width:1250px){.warehouse-transfer-form--relabel{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.warehouse-transfer-form .warehouse-template-actions{justify-content:flex-start}.warehouse-transfer-form .warehouse-labeling-heading{align-items:flex-start;flex-direction:column}}
2026-08-21 17:20:51 +08:00
</style>