2466 lines
270 KiB
Vue
2466 lines
270 KiB
Vue
<script setup lang="ts">
|
||
import { getRelabelQuantities, type RelabelQuantity } from '../api/relabelQuantities'
|
||
import TransferPickupProgressPanel from '../components/TransferPickupProgressPanel.vue'
|
||
import TransferPaneDivider from '../components/TransferPaneDivider.vue'
|
||
import { computed, nextTick, onActivated, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { ElForm, ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||
import { ArrowLeft, ChevronDown, Download, Plus, Printer, Trash2, Upload, X } from '@lucide/vue'
|
||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
||
import type { TableAction } from '@/components/common/tableActions'
|
||
import { useAuthStore } from '@/modules/auth/public'
|
||
import { prepareTransferCartonLabels } from '../utils/transferCartonPrinting'
|
||
import CompletionConfirmationDialog from '../components/CompletionConfirmationDialog.vue'
|
||
import ReviewRejectionNotice from '../components/ReviewRejectionNotice.vue'
|
||
import { returnRelabelCandidate } from '../utils/returnRelabelCandidates'
|
||
import { groupDiscardDocuments } from '../utils/discardDocuments'
|
||
import {
|
||
createDiscard, createRelabel, createTransfer, getDiscard, getRelabel, getTransfer, resubmitRelabel,
|
||
getTransferCartonLabel, submitTransfer, updateTransfer, increaseTransferForecastCartons, getTransferSourceCartons, type TransferSourceCarton,
|
||
downloadRelabelCompletionPhoto, downloadTransferMeasurementPhoto, downloadDiscardPhoto, type DiscardPhoto, uploadRelabelShippingLabel,
|
||
uploadRelabelFile,
|
||
listDiscards, listDiscardCandidates, listRelabelInventoryCandidates, listRelabelHistoricalOrders, listRelabels, listTransfers, cancelDiscard,
|
||
type RelabelInventoryCandidate,
|
||
type RelabelCreate,
|
||
type RelabelDetail,
|
||
type TransferCreate,
|
||
} from '../api/warehouseServices'
|
||
import {
|
||
getMerchantWarehouseDetail,
|
||
getMerchantWarehouses,
|
||
type MerchantWarehouse,
|
||
type MerchantWarehouseDetail,
|
||
} from '@/modules/first-leg/api/firstLeg'
|
||
import { loadAllWarehouseRecords } from '@/modules/first-leg/utils/warehousePagination'
|
||
import { createIdempotencyKey } from '@/utils/idempotencyKey'
|
||
import RelabelDocumentsPanel from '../components/RelabelDocumentsPanel.vue'
|
||
import { relabelSkuCodes, relabelSkuCount } from '../utils/relabelSkuDisplay'
|
||
|
||
type ServiceType = 'labeling' | 'transfer' | 'disposal' | 'relabel'
|
||
type PageMode = 'list' | 'create' | 'edit' | 'detail'
|
||
type LabelingMode = 'carton' | 'sku' | 'carton_and_sku'
|
||
type RelabelTemplateType = 'carton' | 'sku' | 'mixed'
|
||
interface CargoRow { generatedCartonMarks?: string[]; discardCartonIds?: string[]; id: number; included?: boolean; sku: string; name: string; quantity: number; expectedPackageCount?: number; availableQuantity?: number; sourceOrderId?: string; sourceType?: 'DROPSHIP' | 'RETURN_CARTON' | 'RETURN_PARCEL' | 'INVENTORY'; sourceInventoryBalanceId?: string; sourceInventoryPositionId?: string; sourceInboundOrderId?: string; returnCartonId?: 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 }
|
||
interface SkuRelabelRow { id: number; sourceRowId: number | null; cartonMark: string; originalSku: string; newSku: string; quantity: number; fbaCartonMark: string; shipmentNo: string; destinationWarehouseId: string; remark: string }
|
||
interface ImportedRelabelBinding extends SkuRelabelRow { templateType: RelabelTemplateType; selected: boolean; shipmentNo: string }
|
||
interface ServiceConfig { title: string; createTitle: string; numberLabel: string; emptyText: string; columns: string[] }
|
||
interface TransferCarton { items?: Array<{ transferItemId: string | number; skuCode: string; quantity: number }>; cartonMark: string; customerCartonMark?: string; cartonIndex: number; totalCartons: number; status: string; transferItemId?: string | number; transferPalletId?: string | number; length?: number; width?: number; height?: number; weight?: number; quantity?: number; discrepancyReason?: string; countedAt?: string }
|
||
|
||
const props = withDefaults(defineProps<{ service: ServiceType; createPage?: boolean; detailPage?: boolean; detailNo?: string }>(), { createPage: false, detailPage: false, detailNo: '' })
|
||
const auth = useAuthStore()
|
||
const router = useRouter()
|
||
const route = useRoute()
|
||
const transferPaneShare = ref(43)
|
||
// 缓存列表不订阅其他详情页的路由参数,避免离开时清空筛选并重复请求。
|
||
const detailNumber = computed(() => props.detailPage ? props.detailNo || String(route.params.detailNo || '') : '')
|
||
const canCreate = computed(() => auth.hasPermission(({
|
||
labeling: 'merchant:overseas-warehouse:relabel:create',
|
||
transfer: 'merchant:overseas-warehouse:transfer:create',
|
||
disposal: 'merchant:overseas-warehouse:discard:create',
|
||
relabel: 'merchant:overseas-warehouse:return-order:create',
|
||
} as Record<ServiceType, string>)[props.service]))
|
||
const canManageRelabelFiles = computed(() => auth.hasPermission('merchant:overseas-warehouse:relabel:files'))
|
||
const canConfirmRelabel = computed(() => auth.hasPermission('merchant:overseas-warehouse:relabel:confirm'))
|
||
const canConfirmTransfer = computed(() => auth.hasPermission('merchant:overseas-warehouse:transfer:confirm'))
|
||
const canConfirmDiscard = computed(() => auth.hasPermission('merchant:overseas-warehouse:discard:confirm'))
|
||
const completionConfirmOpen = ref(false)
|
||
const completionConfirmNo = ref('')
|
||
const completionConfirmKind = ref<'disposal' | 'labeling' | 'transfer'>('disposal')
|
||
function openCompletionConfirmation(no: string, kind: 'disposal' | 'labeling' | 'transfer') {
|
||
if (!no) return
|
||
completionConfirmNo.value = no; completionConfirmKind.value = kind; completionConfirmOpen.value = true
|
||
}
|
||
async function refreshAfterCompletion(no: string) {
|
||
if (mode.value === 'detail') await openDetail({ '0': no })
|
||
else await load()
|
||
}
|
||
const canCancelDiscard = computed(() => auth.hasPermission('merchant:overseas-warehouse:discard:cancel'))
|
||
const configs: Record<ServiceType, ServiceConfig> = {
|
||
labeling: { title: '贴标申请', createTitle: '新增贴标申请', numberLabel: '贴标申请单号', emptyText: '暂无贴标申请', columns: ['操作', '贴标申请单号', '关联业务单号', '海外操作仓', 'SKU 数(种)', '箱数', '贴标件数', '换标类型', '状态', '创建时间'] },
|
||
transfer: { title: '仓储转运单', createTitle: '新增仓储转运单', numberLabel: '转运单号', emptyText: '暂无仓储转运订单', columns: ['操作', '转运单号', '预计箱数', '海外仓', 'FBA 目的仓', '入仓方式', '是否换标', '换标单号', '换标状态', '状态', '创建时间'] },
|
||
disposal: { title: '货物废弃', createTitle: '新增货物废弃申请', numberLabel: '废弃申请单号', emptyText: '暂无货物废弃申请', columns: ['操作', '废弃申请单号', '废弃出库单号', '进仓编号', '操作仓', 'SKU 汇总', '申请箱数', '预估费用', '状态', '废弃完成时间', '创建时间'] },
|
||
relabel: { title: '退货换标', createTitle: '新建退货换标单', numberLabel: '退货换标单号', emptyText: '暂无退货换标订单', columns: ['操作', '退货换标单号', '来源 FBA 仓', '目的 FBA 仓', 'SKU 汇总', '退件总数', '是否换标', '换标状态', '尾程状态', '审核时间', '测量时间', '出库时间', '创建时间'] },
|
||
}
|
||
const config = computed(() => configs[props.service])
|
||
const listQueryModel = computed<QueryModel>({
|
||
get: () => ({ ...filters, createdDates: [filters.start, filters.end].filter(Boolean), reviewDates: [filters.reviewStart, filters.reviewEnd].filter(Boolean), completedDates: [filters.completedStart, filters.completedEnd].filter(Boolean), measuredDates: [filters.measuredStart, filters.measuredEnd].filter(Boolean), outboundDates: [filters.outboundStart, filters.outboundEnd].filter(Boolean) }),
|
||
set: (value) => Object.assign(filters, {
|
||
number: String(value.number || ''), inboundNo: String(value.inboundNo || ''), warehouse: String(value.warehouse || ''),
|
||
destination: String(value.destination || ''), status: String(value.status || ''),
|
||
start: Array.isArray(value.createdDates) ? String(value.createdDates[0] || '') : '',
|
||
end: Array.isArray(value.createdDates) ? String(value.createdDates[1] || '') : '',
|
||
reviewStart: Array.isArray(value.reviewDates) ? String(value.reviewDates[0] || '') : '',
|
||
reviewEnd: Array.isArray(value.reviewDates) ? String(value.reviewDates[1] || '') : '',
|
||
completedStart: Array.isArray(value.completedDates) ? String(value.completedDates[0] || '') : '',
|
||
completedEnd: Array.isArray(value.completedDates) ? String(value.completedDates[1] || '') : '',
|
||
measuredStart: Array.isArray(value.measuredDates) ? String(value.measuredDates[0] || '') : '',
|
||
measuredEnd: Array.isArray(value.measuredDates) ? String(value.measuredDates[1] || '') : '',
|
||
outboundStart: Array.isArray(value.outboundDates) ? String(value.outboundDates[0] || '') : '',
|
||
outboundEnd: Array.isArray(value.outboundDates) ? String(value.outboundDates[1] || '') : '',
|
||
}),
|
||
})
|
||
const statusOptions = computed(() => props.service === 'labeling'
|
||
? [{ label: '待审核', value: 'PENDING_REVIEW' }, { label: '待处理', value: 'PENDING_PROCESSING' }, { label: '处理中', value: 'PROCESSING' }, { label: '换标完成', value: 'COMPLETED' }, { label: '已驳回', value: 'REJECTED' }]
|
||
: props.service === 'disposal'
|
||
? [{ label: '待审核', value: 'PENDING_REVIEW' }, { label: '待 PDA 废弃', value: 'APPROVED' }, { label: '待客户确认', value: 'PDA_COMPLETED' }, { label: '已完成', value: 'COMPLETED' }, { label: '已拒绝', value: 'REJECTED' }, { label: '已取消', value: 'CANCELLED' }]
|
||
: props.service === 'transfer'
|
||
? [{ label: '草稿', value: 'DRAFT' }, { label: '待审核', value: 'PENDING_REVIEW' },
|
||
{ label: '待PDA收货', value: 'WAIT_PDA_RECEIVING' }, { label: '入库测量中', value: 'INBOUND_MEASURING' },
|
||
{ label: '待客户确认实测', value: 'WAIT_CUSTOMER_CONFIRMATION' }, { label: '待换标', value: 'WAIT_RELABEL' },
|
||
{ label: '待确认目的地', value: 'WAIT_FBA_DESTINATION' }, { label: '待排车', value: 'READY_FOR_DISPATCH' },
|
||
{ label: '待出库', value: 'WAIT_OUTBOUND' }, { label: '已出库', value: 'OUTBOUNDED' },
|
||
{ label: '部分收货', value: 'PARTIALLY_RECEIVED' }, { label: '已收货', value: 'RECEIVED' },
|
||
{ 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>(props.detailPage ? 'detail' : props.createPage ? 'create' : 'list')
|
||
const filters = reactive({ number: '', inboundNo: '', warehouse: '', destination: '', status: '', start: '', end: '', reviewStart: '', reviewEnd: '', completedStart: '', completedEnd: '', measuredStart: '', measuredEnd: '', outboundStart: '', outboundEnd: '' })
|
||
const form = reactive({ customerNo: '', inboundNo: '', sourceWarehouse: '', warehouse: '', destination: '', privateDestinationAddress: '', privateDestinationPostalCode: '', privateDestinationContact: '', privateDestinationPhone: '', address: '', contact: '', phone: '', pickupContact: '', pickupPhone: '', relabelContact: '', relabelPhone: '', deliveryMethod: props.service === 'transfer' ? 'WAREHOUSE_PICKUP' : '', relabelDeliveryMethod: '', trackingNo: '', reason: '', requirement: '', relabel: false, remark: '', labelDeadline: '', estimatedArrivalAt: '', estimatedShipAt: '', fbaShipmentNo: '', volumeCbm: 0.01 })
|
||
const transferDestinationType = ref<'NONE' | 'FBA' | 'PRIVATE'>('NONE')
|
||
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(', ')
|
||
})
|
||
const cargoRows = ref<CargoRow[]>([])
|
||
const transferRelabelRows = ref<CargoRow[]>([])
|
||
const transferRelabelDefaultDestination = ref('')
|
||
const labelingMode = ref<LabelingMode>('carton')
|
||
const shippingLabelFile = ref<File | null>(null)
|
||
const skuRelabelRows = ref<SkuRelabelRow[]>([])
|
||
const importedRelabelBindings = ref<ImportedRelabelBinding[]>([])
|
||
const cartonDetailRows = computed(() => cargoRows.value.filter((row) => row.included))
|
||
const submittedRows = ref<Record<string, string>[]>([])
|
||
const page = ref(1)
|
||
const pageSize = ref(15)
|
||
const total = ref(0)
|
||
const editingTransferNo = ref('')
|
||
interface TransferPallet { id?: string | number; sequenceNo?: number; palletNo?: string; warehouseLocationCode?: string; cartonCount?: number; lengthCm?: number; widthCm?: number; heightCm?: number; grossWeightKg?: number; volumeCbm?: number; status?: string; completedAt?: string }
|
||
interface TransferMeasurementPhoto { fileId: string | number; fileName?: string; contentType?: string; sizeBytes?: number; uploadedAt?: string }
|
||
interface ServiceDetail extends Record<string, unknown> {
|
||
reviewRemark?: string
|
||
discardNo?: string; photos?: DiscardPhoto[]
|
||
transferNo?: string; status?: string; operatingWarehouseId?: string; fbaWarehouseCode?: string
|
||
destinationType?: 'NONE' | 'FBA' | 'PRIVATE'; destinationWarehouseCode?: string; destinationWarehouseName?: string
|
||
privateDestinationAddress?: string; privateDestinationPostalCode?: string; privateDestinationContactName?: string; privateDestinationContactPhone?: string
|
||
deliveryAddress?: string; pickupContactName?: string; pickupContactPhone?: string
|
||
packageCount?: number; volumeCbm?: number; deliveryMethod?: string; estimatedArrivalAt?: string
|
||
estimatedShipAt?: string; actualShipAt?: string; fbaShipmentNo?: string; trackingNo?: string
|
||
remark?: string; needRelabel?: boolean; labelDeadline?: string; relabelContactName?: string
|
||
relabelContactPhone?: string; relabelDeliveryMethod?: string; relabelNo?: string
|
||
relabelStatus?: string; relabelFulfillmentStatus?: string; createdAt?: string
|
||
inboundCode?: string; sourceOrders?: Array<{ lastlegOrderId?: string | number }>
|
||
items?: Array<{ id?: string | number; actualCartonMarks?: string[]; palletNo?: string; cartonMarks?: string[]; inboundCode?: string; productName?: string; discardPackages?: number; quantityUnit?: string; actualDiscardPackages?: number | null; skuCode?: string; productNameZh?: string; quantity?: number | null; packageCount?: number; shippingMark?: string }>
|
||
relabelBindings?: Array<{ operationType?: string; customerCartonMark?: string; originalSku?: string; newSku?: string; quantity?: number; fbaCartonMark?: string; destinationWarehouseCode?: string; remark?: string }>
|
||
cartons?: TransferCarton[]; pallets?: TransferPallet[]; measurementPhotos?: TransferMeasurementPhoto[]; customerConfirmedAt?: string
|
||
}
|
||
const selectedDetail = ref<ServiceDetail | null>(null)
|
||
/** 仅汇总客户原始预报件数,旧单缺失时明确提示,不用实收件数冒充。 */
|
||
const transferForecastQuantity = computed(() => {
|
||
const items = selectedDetail.value?.items || []
|
||
if (!items.length || items.some(item => item.quantity == null)) return '未记录'
|
||
return items.reduce((sum, item) => sum + Number(item.quantity), 0) + ' 件'
|
||
})
|
||
|
||
// 预报箱唛在提交后即可打印,与PDA实收清点明细分别展示。
|
||
const showForecastCartons = ref(false)
|
||
const increaseForecastOpen = ref(false)
|
||
const increasingForecast = ref(false)
|
||
const forecastSnapshot = ref<ServiceDetail | null>(null)
|
||
const increaseForecastForm = reactive({ additionalCount: 1 })
|
||
const canIncreaseForecast = computed(() => canCreate.value && selectedDetail.value?.transferNo === selectedDetail.value?.inboundCode && ['PENDING_REVIEW', 'WAIT_PDA_RECEIVING'].includes(selectedDetail.value?.status || ''))
|
||
/** 保存原预报快照,防止弹窗打开后其他设备的修改被覆盖。 */
|
||
function openIncreaseForecast() {
|
||
if (!canIncreaseForecast.value || !selectedDetail.value) return
|
||
forecastSnapshot.value = selectedDetail.value
|
||
increaseForecastForm.additionalCount = 1; increaseForecastOpen.value = true
|
||
}
|
||
/** 只补箱唛,SKU预报件数不随包装箱数自动改变。 */
|
||
async function saveIncreaseForecast() {
|
||
if (increasingForecast.value || !forecastSnapshot.value?.transferNo) return
|
||
const snapshot = forecastSnapshot.value
|
||
if (!Number.isSafeInteger(increaseForecastForm.additionalCount) || increaseForecastForm.additionalCount < 1) { ElMessage.warning('请填写有效新增箱数'); return }
|
||
increasingForecast.value = true
|
||
try {
|
||
const result = await increaseTransferForecastCartons(String(snapshot.transferNo), { expectedPackageCount: Number(snapshot.packageCount), packageCount: Number(snapshot.packageCount) + increaseForecastForm.additionalCount })
|
||
selectedDetail.value = result as ServiceDetail; increaseForecastOpen.value = false
|
||
ElMessage.success('预报箱数已增加,新增箱唛可打印;请通知PDA刷新后继续清点')
|
||
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '增加预报箱数失败') }
|
||
finally { increasingForecast.value = false }
|
||
}
|
||
const showTransferCartons = ref(false)
|
||
function transferCartonItem(carton: TransferCarton) {
|
||
return selectedDetail.value?.items?.find((item) => carton.transferItemId != null && String(item.id) === String(carton.transferItemId))
|
||
}
|
||
function transferCartonLines(carton: TransferCarton) {
|
||
if (carton.items?.length) return carton.items.map(line => ({ ...line, productNameZh: selectedDetail.value?.items?.find(item => String(item.id) === String(line.transferItemId))?.productNameZh || '—' }))
|
||
const item = transferCartonItem(carton)
|
||
return [{ transferItemId: carton.transferItemId, skuCode: item?.skuCode || '—', productNameZh: item?.productNameZh || '—', quantity: Number(carton.quantity || 0) }]
|
||
}
|
||
function palletCartons(pallet: TransferPallet) {
|
||
return countedTransferCartons().filter((carton) => pallet.id != null && carton.transferPalletId != null && String(carton.transferPalletId) === String(pallet.id))
|
||
}
|
||
function palletSkuRows(pallet: TransferPallet) {
|
||
const summary = new Map<string, number>()
|
||
for (const carton of palletCartons(pallet)) {
|
||
for (const line of transferCartonLines(carton)) summary.set(line.skuCode, (summary.get(line.skuCode) || 0) + Number(line.quantity))
|
||
}
|
||
return [...summary.entries()].map(([sku, quantity]) => ({ sku, quantity }))
|
||
}
|
||
function palletQuantity(pallet: TransferPallet) { return palletCartons(pallet).reduce((sum, carton) => sum + Number(carton.quantity || 0), 0) }
|
||
/** 保留历史实收,后续占用、废弃与出库不回退签收事实。 */
|
||
function countedTransferCartons() { return (selectedDetail.value?.cartons || []).filter((carton) => ['COUNTED', 'DISCARD_PENDING', 'DISCARDED', 'SHIPPED'].includes(carton.status)) }
|
||
function receivedItemCartons(itemId: string | number | undefined) {
|
||
return countedTransferCartons().filter(carton => itemId != null && transferCartonLines(carton).some(line => String(line.transferItemId) === String(itemId)))
|
||
}
|
||
function receivedItemQuantity(itemId: string | number | undefined) {
|
||
return receivedItemCartons(itemId).reduce((sum, carton) => sum + transferCartonLines(carton).filter(line => String(line.transferItemId) === String(itemId)).reduce((total, line) => total + Number(line.quantity), 0), 0)
|
||
}
|
||
|
||
const detailError = ref('')
|
||
const selectedRelabelDetail = ref<RelabelDetail | null>(null)
|
||
const selectedRelabelQuantity = ref<RelabelQuantity | null>(null)
|
||
const completionPhotoUrls = reactive<Record<string, string>>({})
|
||
const completionPhotoLoading = reactive<Record<string, boolean>>({})
|
||
const completionPhotoErrors = reactive<Record<string, boolean>>({})
|
||
const completionPhotoPreviewUrls = computed(() => (selectedRelabelDetail.value?.completionEvidence?.photoFileIds || [])
|
||
.map((fileId) => completionPhotoUrls[fileId]).filter(Boolean))
|
||
const measurementPhotoUrls = reactive<Record<string, string>>({})
|
||
const measurementPhotoLoading = reactive<Record<string, boolean>>({})
|
||
const measurementPhotoErrors = reactive<Record<string, boolean>>({})
|
||
const measurementPhotoPreviewUrls = computed(() => (selectedDetail.value?.measurementPhotos || [])
|
||
.map((photo) => measurementPhotoUrls[photo.fileId]).filter(Boolean))
|
||
const discardPhotoUrls = reactive<Record<string, string>>({})
|
||
const discardPhotoLoading = reactive<Record<string, boolean>>({})
|
||
const discardPhotoErrors = reactive<Record<string, boolean>>({})
|
||
const discardPhotoPreviewUrls = computed(() => (selectedDetail.value?.photos || []).map(photo => discardPhotoUrls[photo.fileId]).filter(Boolean))
|
||
let discardPhotoGeneration = 0
|
||
function clearDiscardPhotos() { discardPhotoGeneration++; Object.values(discardPhotoUrls).forEach(URL.revokeObjectURL); for (const map of [discardPhotoUrls, discardPhotoLoading, discardPhotoErrors]) Object.keys(map).forEach(key => delete map[key]) }
|
||
async function loadDiscardPhoto(fileId: string) {
|
||
const current = selectedDetail.value; const generation = discardPhotoGeneration
|
||
if (!current?.discardNo || discardPhotoLoading[fileId]) return
|
||
discardPhotoLoading[fileId] = true; discardPhotoErrors[fileId] = false
|
||
try { const blob = await downloadDiscardPhoto(current.discardNo, fileId); if (generation !== discardPhotoGeneration || selectedDetail.value !== current || mode.value !== 'detail') return; if (discardPhotoUrls[fileId]) URL.revokeObjectURL(discardPhotoUrls[fileId]); discardPhotoUrls[fileId] = URL.createObjectURL(blob) }
|
||
catch { if (generation === discardPhotoGeneration) discardPhotoErrors[fileId] = true }
|
||
finally { if (generation === discardPhotoGeneration) discardPhotoLoading[fileId] = false }
|
||
}
|
||
async function loadDiscardPhotos() { clearDiscardPhotos(); await Promise.all((selectedDetail.value?.photos || []).map(photo => loadDiscardPhoto(photo.fileId))) }
|
||
const fbaWarehouses = ref<MerchantWarehouse[]>([])
|
||
const operationWarehouses = ref<MerchantWarehouse[]>([])
|
||
const fbaWarehousesLoading = ref(false)
|
||
const fbaWarehousesError = ref('')
|
||
const operationWarehousesError = ref('')
|
||
const listQueryFields = computed<QueryField[]>(() => {
|
||
const fields: QueryField[] = [
|
||
{ key: 'number', label: config.value.numberLabel, type: 'input', placeholder: `请输入${config.value.numberLabel}` },
|
||
]
|
||
if (props.service !== 'labeling' && props.service !== 'relabel') fields.push({ key: 'inboundNo', label: '进仓编号', type: 'input', placeholder: '请输入进仓编号' })
|
||
fields.push({
|
||
key: 'warehouse', label: props.service === 'relabel' ? '来源 FBA 仓' : '操作仓', type: 'select',
|
||
options: (props.service === 'relabel' ? fbaWarehouses.value : operationWarehouses.value).map((warehouse) => ({ label: warehouseLabel(warehouse), value: String(warehouse.id) })),
|
||
})
|
||
if (props.service === 'transfer' || props.service === 'relabel') fields.push({ key: 'destination', label: '目的 FBA 仓', type: 'input', placeholder: '请输入仓代码' })
|
||
fields.push({ key: 'status', label: '订单状态', type: 'select', options: statusOptions.value })
|
||
fields.push({ key: 'createdDates', label: '创建时间', type: 'daterange' })
|
||
if (props.service === 'relabel') fields.push(
|
||
{ key: 'reviewDates', label: '审核时间', type: 'daterange' },
|
||
{ key: 'measuredDates', label: '测量时间', type: 'daterange' },
|
||
{ key: 'outboundDates', label: '出库时间', type: 'daterange' },
|
||
)
|
||
return fields
|
||
})
|
||
const listColumns = computed<DataTableColumn[]>(() => config.value.columns.map((label, index) => index === 0
|
||
? { key: 'actions', label, slotName: 'actions', width: props.service === 'transfer' ? 210 : 160, fixed: 'left' }
|
||
: { key: String(index - 1), prop: String(index - 1), label, minWidth: label === '关联业务单号' ? 300 : index === 1 || label === '换标单号' ? 190 : 130,
|
||
singleLine: label !== '关联业务单号', className: label === '关联业务单号' ? 'related-business-number-cell' : undefined,
|
||
slotName: props.service === 'labeling' && ['SKU 数(种)', '箱数', '贴标件数'].includes(label) ? 'labelingQuantity' : props.service === 'labeling' && label === '换标类型' ? 'labelingType' : props.service === 'labeling' && label === '状态' ? 'labelingStatus' : props.service === 'transfer' && label === '换标状态' ? 'transferRelabelStatus' : props.service === 'transfer' && label === '状态' ? 'transferStatus' : undefined }))
|
||
const discardRequestedPackages = ref<number | undefined>()
|
||
const discardQuantityMode = computed(() => Boolean(discardSelectedCandidate.value?.sources.length && discardSelectedCandidate.value.sources.every(source => source.sourceType === 'INVENTORY' && !source.cartonSelectionSupported && Boolean(source.palletNo))))
|
||
const discardSelectedBoxKeys = ref<string[]>([])
|
||
const discardBoxSearch = ref('')
|
||
const discardCandidates = ref<import('../api/warehouseServices').DiscardCandidate[]>([])
|
||
const discardDocuments = computed(() => groupDiscardDocuments(discardCandidates.value))
|
||
const discardSelectedCandidate = computed(() => discardDocuments.value.find(item => item.orderId === selectedDiscardOrderId.value))
|
||
const discardSelectionSources = computed(() => discardSelectedCandidate.value?.sources || [])
|
||
const discardSourceBoxes = computed(() => discardSelectionSources.value.flatMap(source => (source.cartons || []).map(box => ({ ...box, key: `${source.sourceType}:${box.cartonId}`, source }))))
|
||
const discardSelectableBoxes = computed(() => [...new Map(discardSourceBoxes.value.map(box => [box.key, box])).values()])
|
||
const discardVisibleBoxes = computed(() => discardSelectableBoxes.value.filter(box => !discardBoxSearch.value.trim() || box.cartonMark.includes(discardBoxSearch.value.trim()) || box.skus.some(sku => sku.skuCode.includes(discardBoxSearch.value.trim()))))
|
||
function toggleDiscardBox(key: string, selected: boolean) {
|
||
discardSelectedBoxKeys.value = selected ? [...new Set([...discardSelectedBoxKeys.value, key])] : discardSelectedBoxKeys.value.filter(value => value !== key)
|
||
syncDiscardSelection()
|
||
}
|
||
function selectAllDiscardBoxes(selected: boolean) {
|
||
discardSelectedBoxKeys.value = selected ? discardSelectableBoxes.value.map(box => box.key) : []
|
||
syncDiscardSelection()
|
||
}
|
||
function syncDiscardSelection() {
|
||
const selected = discardSourceBoxes.value.filter(box => discardSelectedBoxKeys.value.includes(box.key))
|
||
const groups = new Map<string, CargoRow>()
|
||
for (const box of selected) {
|
||
const source = box.source
|
||
const skus = source.sourceType === 'RETURN_CARTON' ? source.skus : source.sourceType === 'INVENTORY' ? source.skus.filter(sku => box.skus.some(content => content.skuCode === sku.skuCode)) : box.skus.slice(0, 1)
|
||
for (const sku of skus) {
|
||
const key = `${source.sourceType}:${source.inventoryPositionId || source.returnCartonId || source.orderId}:${sku.skuCode}`
|
||
let row = groups.get(key)
|
||
if (!row) {
|
||
row = { ...createCargoRow(), sku: sku.skuCode, name: sku.productName, quantity: 0, availableQuantity: sku.availablePackages,
|
||
sourceOrderId: source.orderId, sourceType: source.sourceType, sourceInventoryPositionId: source.inventoryPositionId, returnCartonId: source.returnCartonId,
|
||
palletNo: source.palletNo, discardCartonIds: source.sourceType === 'RETURN_CARTON' ? undefined : [] }
|
||
groups.set(key, row)
|
||
}
|
||
row.quantity += source.sourceType === 'RETURN_CARTON' ? sku.availablePackages : 1
|
||
row.discardCartonIds?.push(box.cartonId)
|
||
}
|
||
}
|
||
cargoRows.value = [...groups.values()]
|
||
}
|
||
|
||
const selectedDiscardOrderId = ref('')
|
||
const discardCandidatesLoading = ref(false)
|
||
const relabelCandidates = ref<RelabelInventoryCandidate[]>([])
|
||
const selectedRelabelInboundOrderId = ref('')
|
||
const isLabelingCreate = computed(() => props.service === 'labeling' && mode.value === 'create')
|
||
const isTransferCreate = computed(() => props.service === 'transfer' && mode.value === 'create')
|
||
const isDisposalCreate = computed(() => props.service === 'disposal' && mode.value === 'create')
|
||
const isServiceCreate = computed(() => isLabelingCreate.value || isTransferCreate.value || isDisposalCreate.value)
|
||
const disposalFormModel = computed(() => ({ ...form, sourceOrder: selectedDiscardOrderId.value, requestedPackages: discardRequestedPackages.value }))
|
||
const disposalFormRules: FormRules = {
|
||
warehouse: [{ required: true, message: '请选择操作仓', trigger: 'change' }],
|
||
sourceOrder: [{ required: true, message: '请选择入库单', trigger: 'change' }],
|
||
reason: [{ required: true, whitespace: true, message: '请选择废弃原因', trigger: 'change' }],
|
||
}
|
||
const labelingFormRef = ref<FormInstance>()
|
||
const labelingFormModel = computed(() => ({ ...form, sourceOrder: selectedRelabelInboundOrderId.value, shippingLabelFile: shippingLabelFile.value }))
|
||
const labelingFormRules = computed<FormRules>(() => ({
|
||
warehouse: [{ required: true, message: '请选择操作仓', trigger: 'change' }],
|
||
sourceOrder: [{ required: true, message: '请选择入仓单或退货单', trigger: 'change' }],
|
||
contact: [{ required: true, whitespace: true, message: '请输入签收联系人', trigger: 'blur' }],
|
||
phone: [{ required: true, whitespace: true, message: '请输入联系电话', trigger: 'blur' }],
|
||
labelDeadline: [{ required: true, message: '请选择贴标截止时间', trigger: 'change' }],
|
||
destination: labelingMode.value === 'sku' ? [] : [{ required: true, message: '请选择 FBA 目的仓', trigger: 'change' }],
|
||
deliveryMethod: labelingMode.value === 'sku' ? [] : [{ required: true, message: '请选择派送方式', trigger: 'change' }],
|
||
shippingLabelFile: labelingMode.value !== 'sku' && form.deliveryMethod === 'EXPRESS' && form.relabel
|
||
? [{ required: true, type: 'object', message: '请上传面单文件', trigger: 'change' }] : [],
|
||
}))
|
||
const orderInfoCollapsed = ref(false)
|
||
watch([labelingMode, () => form.deliveryMethod, () => form.relabel], () => {
|
||
if (isLabelingCreate.value) labelingFormRef.value?.clearValidate(['destination', 'deliveryMethod', 'shippingLabelFile'])
|
||
})
|
||
const availableBoxesCollapsed = ref(false)
|
||
const availableSkusCollapsed = ref(false)
|
||
const relabelCandidatesLoading = ref(false)
|
||
const transferInventoryCandidates = ref<RelabelInventoryCandidate[]>([])
|
||
const transferAvailableSourceCounts = ref<Record<string, number>>({})
|
||
const selectedTransferInventory = ref('')
|
||
const inventoryTransfer = computed(() => props.service === 'transfer' && Boolean(selectedTransferInventory.value))
|
||
// 来源库存发往FBA时默认贴标,私人仓和外来货保留原有选择。
|
||
const transferRelabelRequired = computed(() => inventoryTransfer.value && transferDestinationType.value === 'FBA')
|
||
watch(transferRelabelRequired, (required) => { if (required) form.relabel = true }, { flush: 'sync' })
|
||
const transferSourceCartons = ref<TransferSourceCarton[]>([])
|
||
const transferSourceLoading = ref(false)
|
||
const transferSourceError = ref('')
|
||
let transferSourceRequest = 0
|
||
const transferFormModel = computed(() => ({ ...form, relabelDestination: transferRelabelDefaultDestination.value }))
|
||
const transferFormRules = computed<FormRules>(() => {
|
||
const required = (message: string, enabled = true, trigger = 'change') => enabled ? [{ required: true, whitespace: true, message, trigger }] : []
|
||
const incoming = !inventoryTransfer.value
|
||
const pickup = incoming && form.deliveryMethod === 'WAREHOUSE_PICKUP'
|
||
const privateDestination = transferDestinationType.value === 'PRIVATE'
|
||
return {
|
||
warehouse: required('请选择入库海外仓'),
|
||
deliveryMethod: required('请选择入仓方式', incoming),
|
||
address: required(form.deliveryMethod === 'WAREHOUSE_PICKUP' ? '请输入提货地址' : '请输入送货地址', incoming, 'blur'),
|
||
pickupContact: required('请输入提货联系人', pickup, 'blur'),
|
||
pickupPhone: required('请输入联系电话', pickup, 'blur'),
|
||
estimatedArrivalAt: required('请选择预计入仓时间', incoming),
|
||
destination: required('请选择 FBA 目的仓', transferDestinationType.value === 'FBA'),
|
||
privateDestinationAddress: required('请输入私人仓地址', privateDestination, 'blur'),
|
||
privateDestinationPostalCode: required('请输入邮编', privateDestination, 'blur'),
|
||
privateDestinationContact: required('请输入联系人', privateDestination, 'blur'),
|
||
privateDestinationPhone: required('请输入联系电话', privateDestination, 'blur'),
|
||
labelDeadline: required('请选择换标截止时间', form.relabel),
|
||
relabelContact: required('请输入换标联系人', form.relabel, 'blur'),
|
||
relabelPhone: required('请输入联系电话', form.relabel, 'blur'),
|
||
relabelDeliveryMethod: required('请选择派送方式', form.relabel && labelingMode.value !== 'sku'),
|
||
relabelDestination: required('请选择 FBA 目的仓', form.relabel),
|
||
}
|
||
})
|
||
watch([inventoryTransfer, transferDestinationType, labelingMode, () => form.deliveryMethod, () => form.relabel], () => {
|
||
if (isTransferCreate.value) labelingFormRef.value?.clearValidate()
|
||
})
|
||
async function loadTransferSourceCartons() {
|
||
const request = ++transferSourceRequest
|
||
transferSourceCartons.value = []; transferSourceError.value = ''
|
||
if (!inventoryTransfer.value) return
|
||
transferSourceLoading.value = true
|
||
try {
|
||
// 退货候选本身就是逐箱库存,使用与下拉一致的实物箱数据,不能按普通入仓快照回查。
|
||
if (selectedTransferInventory.value.startsWith('RETURN_ORDER:')) {
|
||
const returnNo = selectedTransferInventory.value.slice('RETURN_ORDER:'.length)
|
||
transferSourceCartons.value = transferInventoryCandidates.value
|
||
.filter(item => !['COMPLETED', 'PARTIALLY_COMPLETED'].includes(item.relabelStatus || '')
|
||
&& Number(item.relabelledQuantity || item.relabelledPackages || 0) === 0)
|
||
.filter(item => (item.sourceType === 'RETURN_CARTON' || item.sourceType === 'RETURN_PARCEL')
|
||
&& String(item.inboundOrderNo || '').trim() === returnNo && Number(item.cartonAvailablePackages || 0) > 0)
|
||
.filter(item => Boolean(item.cartonMark))
|
||
.map(item => {
|
||
const contents = item.cartonSkuItems?.length ? item.cartonSkuItems : item.skuItems || []
|
||
return { cartonMark: item.cartonMark!, productNameZh: item.productNameZh,
|
||
quantity: contents.reduce((total, sku) => total + Number(sku.quantity || 0), 0),
|
||
destinationWarehouseCode: item.destinationFbaWarehouseCode,
|
||
destinationWarehouseName: item.destinationFbaWarehouseName,
|
||
items: contents.map(sku => ({ skuCode: sku.skuCode, quantity: Number(sku.quantity || 0) })) }
|
||
})
|
||
if (!transferSourceCartons.value.length) throw new Error('当前退货单没有可转运箱,请刷新库存后重试')
|
||
return
|
||
}
|
||
const cartons = await getTransferSourceCartons(form.warehouse, selectedTransferInventory.value)
|
||
if (request === transferSourceRequest) transferSourceCartons.value = cartons
|
||
} catch (error) { if (request === transferSourceRequest) transferSourceError.value = error instanceof Error ? error.message : '原单箱明细加载失败' }
|
||
finally { if (request === transferSourceRequest) transferSourceLoading.value = false }
|
||
}
|
||
// Snowflake IDs exceed JavaScript's safe integer range. Keep them as strings
|
||
// all the way to JSON so the backend receives the exact source order ID.
|
||
const selectedTransferSourceOrderIds = ref<string[]>([])
|
||
const isReturnRelabelSource = computed(() => props.service === 'labeling' && selectedRelabelInboundOrderId.value.startsWith('RETURN_ORDER:'))
|
||
const selectedReturnOrderNo = computed(() => isReturnRelabelSource.value ? selectedRelabelInboundOrderId.value.slice('RETURN_ORDER:'.length) : '')
|
||
const currentRelabelCandidates = computed(() => relabelCandidates.value.map(item => returnRelabelCandidate(item, labelingMode.value)))
|
||
const returnCartonQuery = ref(String(route.query.cartonMark || ''))
|
||
const selectedReturnCandidates = computed(() => currentRelabelCandidates.value.filter(item => Boolean(item.returnCartonId)
|
||
&& returnSourceNos(item).includes(selectedReturnOrderNo.value)
|
||
&& (!returnCartonQuery.value.trim() || (item.cartonMark || '').toUpperCase().includes(returnCartonQuery.value.trim().toUpperCase()))))
|
||
const availableReturnCandidates = computed(() => selectedReturnCandidates.value.filter(item => inventoryQuantity(item.availablePackages) > 0).map(item => ({ ...item, skuItems: (item.skuItems || []).filter(sku => inventoryQuantity(sku.quantity) > 0) })))
|
||
const returnBoxCandidates = computed(() => availableReturnCandidates.value.filter(item => item.sourceType === 'RETURN_CARTON'))
|
||
const returnLooseCandidates = computed(() => availableReturnCandidates.value.filter(item => item.sourceType === 'RETURN_PARCEL'))
|
||
const requiresBothTotalFiles = computed(() => (props.service === 'labeling' || (props.service === 'transfer' && form.relabel)) && labelingMode.value === 'carton_and_sku')
|
||
function inventoryQuantity(value: number | string | null | undefined) {
|
||
const quantity = Number(value)
|
||
return Number.isFinite(quantity) && quantity > 0 ? quantity : 0
|
||
}
|
||
const relabelInventorySearch = ref('')
|
||
function returnSourceNos(candidate: RelabelInventoryCandidate): string[] {
|
||
return [...new Set((candidate.inboundOrderNo || '').split(',').map(value => value.trim()).filter(Boolean))]
|
||
}
|
||
function returnRelabelProgress(candidate: RelabelInventoryCandidate) {
|
||
const labels: Record<string, string> = { UNLABELLED: '未换标', BOUND: '处理中', PROCESSING: '处理中', PARTIALLY_COMPLETED: '部分完成', COMPLETED: '全部完成' }
|
||
return [labels[candidate.relabelStatus || ''] || '—', '实际完成 ' + (candidate.relabelledQuantity ?? '—') + ' 件', '处理中占用 ' + (candidate.relabelReservedQuantity ?? '—') + ' 件'].join('|')
|
||
}
|
||
function returnInventoryState(candidate: RelabelInventoryCandidate) {
|
||
const labels: Record<string, string> = { AVAILABLE: '可选库存', RELABEL_PENDING: '换标占用', PACKING_RESERVED: '合箱计划预留', OUTBOUND_PENDING: '出库占用', DISCARD_PENDING: '废弃申请占用', OUTBOUNDED: '已出库', MERGED: '已合箱' }
|
||
return [...(returnSourceNos(candidate).length > 1 ? ['跨来源单合箱,数量为整箱内容'] : []), ...(candidate.mergedIntoCartonMarks?.length ? ['历史部分转入 ' + candidate.mergedIntoCartonMarks.join('、') + '(不代表当前库存位置)'] : []), labels[candidate.inventoryStatus || ''] || (inventoryQuantity(candidate.availablePackages) > 0 ? '可选库存' : '当前不可选'), candidate.occupiedRelabelNo].filter(Boolean).join(' · ')
|
||
}
|
||
const relabelInboundOptions = computed(() => {
|
||
const grouped = new Map<string, { value: string; inboundOrderNo: string; inboundCode: string; availablePackages: number; quantityUnit: '箱' | '件'; stockQuantity: number | null; units: number; hasOutbound: boolean; reasons: string[] }>()
|
||
for (const candidate of currentRelabelCandidates.value) {
|
||
const sources = candidate.returnCartonId ? returnSourceNos(candidate) : [String(candidate.inboundOrderId)]
|
||
for (const sourceNo of sources) {
|
||
const value = candidate.returnCartonId ? 'RETURN_ORDER:' + sourceNo : sourceNo
|
||
const current = grouped.get(value)
|
||
if (current) {
|
||
current.hasOutbound ||= candidate.hasOutbound === true || candidate.inventoryStatus === 'OUTBOUNDED'
|
||
current.availablePackages += inventoryQuantity(candidate.availablePackages)
|
||
current.units += candidate.returnCartonId && Number(candidate.stockQuantity) === 0 ? 0 : 1
|
||
current.stockQuantity = current.stockQuantity == null || candidate.stockQuantity == null ? null : current.stockQuantity + inventoryQuantity(candidate.stockQuantity)
|
||
if (candidate.returnCartonId) current.reasons.push(returnInventoryState(candidate))
|
||
}
|
||
else grouped.set(value, {
|
||
value, inboundOrderNo: candidate.returnCartonId ? '退货单 ' + sourceNo : (candidate.inboundOrderNo || '—'),
|
||
inboundCode: candidate.returnCartonId ? '已签收退货库存(含合箱)' : (candidate.inboundCode || '—'),
|
||
availablePackages: inventoryQuantity(candidate.availablePackages),
|
||
quantityUnit: candidate.returnCartonId ? '件' : '箱',
|
||
stockQuantity: candidate.stockQuantity == null ? null : inventoryQuantity(candidate.stockQuantity), units: candidate.returnCartonId && Number(candidate.stockQuantity) === 0 ? 0 : 1, hasOutbound: candidate.hasOutbound === true || candidate.inventoryStatus === 'OUTBOUNDED', reasons: candidate.returnCartonId ? [returnInventoryState(candidate)] : [],
|
||
})
|
||
}
|
||
}
|
||
return [...grouped.values()].map(option => ({ ...option, reasons: [...new Set(option.reasons)] }))
|
||
})
|
||
const visibleRelabelInboundOptions = computed(() => relabelInboundOptions.value.filter(option => option.availablePackages > 0))
|
||
function relabelOptionLabel(option: typeof relabelInboundOptions.value[number]) {
|
||
if (option.quantityUnit === '箱') return option.inboundOrderNo + '|' + option.inboundCode + '|可选 ' + option.availablePackages + ' 箱'
|
||
return option.inboundOrderNo + '|在仓 ' + option.units + ' 包裹/箱 · ' + (option.stockQuantity ?? '待查询') + ' 件' + (option.reasons.some(reason => reason.includes('跨来源单')) ? '(关联整箱数量,含其他来源)' : '') + (option.hasOutbound && Number(option.stockQuantity) > 0 ? '|部分出库' : '')
|
||
}
|
||
const loading = ref(false)
|
||
const saving = ref(false)
|
||
const unifiedTemplateInput = ref<HTMLInputElement | null>(null)
|
||
const transferCartonMarkInput = ref<HTMLInputElement | null>(null)
|
||
const relabelFileInput = ref<HTMLInputElement | null>(null)
|
||
const relabelFileScope = ref<'CARTON_TOTAL' | 'SKU_TOTAL' | 'ITEM'>('CARTON_TOTAL')
|
||
const relabelFileItemId = ref<string>()
|
||
const cartonTotalFile = ref<File | null>(null)
|
||
const skuTotalFile = ref<File | null>(null)
|
||
const cartonTotalCreateInput = ref<HTMLInputElement | null>(null)
|
||
const skuTotalCreateInput = ref<HTMLInputElement | null>(null)
|
||
let cargoSequence = 0
|
||
let skuRelabelSequence = 0
|
||
|
||
watch(() => [props.service, props.createPage, props.detailPage, detailNumber.value] as const, () => {
|
||
mode.value = props.detailPage ? 'detail' : props.createPage ? 'create' : 'list'
|
||
page.value = 1
|
||
resetFilters()
|
||
resetForm()
|
||
if (props.createPage) void openCreate()
|
||
else if (props.detailPage && detailNumber.value) void loadDetailPage()
|
||
else void loadPage()
|
||
})
|
||
watch(mode, (value) => { if (value !== 'detail') { clearCompletionPhotos(); clearMeasurementPhotos(); clearDiscardPhotos() } })
|
||
watch(() => form.relabel, (value) => {
|
||
if (props.service === 'transfer' && value) {
|
||
labelingMode.value = 'carton_and_sku'
|
||
if (!transferRelabelRows.value.length) addTransferRelabelRow()
|
||
}
|
||
})
|
||
watch(() => cargoRows.value.map((row) => row.sku), (skus) => {
|
||
if (props.service !== 'transfer' || !form.relabel) return
|
||
transferRelabelRows.value.forEach((row, index) => {
|
||
const sourceSku = skus[Math.min(index, Math.max(0, skus.length - 1))]?.trim() || ''
|
||
if (sourceSku) row.sku = sourceSku
|
||
})
|
||
})
|
||
watch(labelingMode, (value) => {
|
||
if (!isReturnRelabelSource.value && value !== 'carton' && !skuRelabelRows.value.length) addSkuRelabelRow()
|
||
})
|
||
watch(() => form.destination, (value) => {
|
||
if (props.service !== 'labeling') return
|
||
for (const row of cargoRows.value) row.destinationWarehouseId = value
|
||
for (const row of skuRelabelRows.value) row.destinationWarehouseId = value
|
||
for (const row of importedRelabelBindings.value) row.destinationWarehouseId = value
|
||
})
|
||
function refreshWarehouseMarkLabels() {
|
||
if (props.service !== 'labeling') return
|
||
const root = document.querySelector('.warehouse-service-page')
|
||
if (!root) return
|
||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
|
||
const nodes: Text[] = []
|
||
let node: Node | null
|
||
while ((node = walker.nextNode())) nodes.push(node as Text)
|
||
for (const text of nodes) text.nodeValue = text.nodeValue?.replaceAll('旧箱唛', '仓库唛头').replaceAll('旧唛头', '仓库唛头') || text.nodeValue
|
||
}
|
||
async function loadPage() {
|
||
if (props.service === 'labeling' || props.service === 'relabel' || props.service === 'transfer' || props.service === 'disposal') await loadWarehouses()
|
||
await load()
|
||
}
|
||
onMounted(() => {
|
||
if (props.createPage) void openCreate()
|
||
else if (props.detailPage && detailNumber.value) void loadDetailPage()
|
||
else void loadPage()
|
||
void nextTick(refreshWarehouseMarkLabels)
|
||
})
|
||
let activatedOnce = false
|
||
onActivated(() => {
|
||
if (activatedOnce && !props.createPage && !props.detailPage) {
|
||
mode.value = 'list'
|
||
void loadPage()
|
||
}
|
||
activatedOnce = true
|
||
})
|
||
onBeforeUnmount(() => { clearCompletionPhotos(); clearMeasurementPhotos(); clearDiscardPhotos() })
|
||
function resetFilters() { Object.assign(filters, { number: '', inboundNo: '', warehouse: '', destination: '', status: '', start: '', end: '', reviewStart: '', reviewEnd: '', completedStart: '', completedEnd: '', measuredStart: '', measuredEnd: '', outboundStart: '', outboundEnd: '' }) }
|
||
function resetForm() {
|
||
labelingFormRef.value?.clearValidate?.()
|
||
orderInfoCollapsed.value = false
|
||
availableBoxesCollapsed.value = false
|
||
availableSkusCollapsed.value = false
|
||
Object.assign(form, { customerNo: '', inboundNo: '', sourceWarehouse: '', warehouse: '', destination: '', privateDestinationAddress: '', privateDestinationPostalCode: '', privateDestinationContact: '', privateDestinationPhone: '', address: '', contact: '', phone: '', pickupContact: '', pickupPhone: '', relabelContact: '', relabelPhone: '', deliveryMethod: props.service === 'transfer' ? 'WAREHOUSE_PICKUP' : '', relabelDeliveryMethod: '', trackingNo: '', reason: '', requirement: '', relabel: false, remark: '', labelDeadline: '', estimatedArrivalAt: '', estimatedShipAt: '', fbaShipmentNo: '', volumeCbm: 0.01 })
|
||
cargoRows.value = []
|
||
transferRelabelRows.value = []
|
||
transferRelabelDefaultDestination.value = ''
|
||
labelingMode.value = 'carton'
|
||
skuRelabelRows.value = []
|
||
importedRelabelBindings.value = []
|
||
discardCandidates.value = []
|
||
selectedDiscardOrderId.value = ''
|
||
relabelCandidates.value = []
|
||
transferInventoryCandidates.value = []
|
||
selectedTransferInventory.value = ''
|
||
selectedTransferSourceOrderIds.value = []
|
||
selectedRelabelInboundOrderId.value = ''
|
||
cartonTotalFile.value = null
|
||
skuTotalFile.value = null
|
||
shippingLabelFile.value = null
|
||
if (cartonTotalCreateInput.value) cartonTotalCreateInput.value.value = ''
|
||
if (skuTotalCreateInput.value) skuTotalCreateInput.value.value = ''
|
||
transferDestinationType.value = 'NONE'
|
||
selectedOperationWarehouse.value = null
|
||
editingTransferNo.value = ''
|
||
}
|
||
async function openCreate() {
|
||
if (!props.createPage && props.service !== 'relabel') {
|
||
await router.push({ name: `${serviceListRouteName(props.service)}-create` })
|
||
return
|
||
}
|
||
resetForm(); mode.value = 'create'
|
||
await loadWarehouses()
|
||
if (props.service === 'labeling') {
|
||
const requestedReturnNo = String(route.query.returnNo || '').trim()
|
||
const requestedWarehouseId = String(route.query.warehouseId || '').trim()
|
||
if (requestedReturnNo && requestedWarehouseId && operationWarehouses.value.some(warehouse => String(warehouse.id) === requestedWarehouseId)) {
|
||
form.warehouse = requestedWarehouseId
|
||
await selectOperationWarehouse()
|
||
const sourceValue = `RETURN_ORDER:${requestedReturnNo}`
|
||
if (relabelInboundOptions.value.some(option => option.value === sourceValue)) {
|
||
selectedRelabelInboundOrderId.value = sourceValue
|
||
selectRelabelInventory(sourceValue)
|
||
} else ElMessage.warning(`退货单 ${requestedReturnNo} 暂无可申请贴标的库存`)
|
||
}
|
||
return
|
||
}
|
||
if (props.service !== 'disposal') { addCargoRow(); return }
|
||
}
|
||
|
||
function returnToList() {
|
||
if (props.createPage || props.detailPage) {
|
||
void router.push({ name: serviceListRouteName(props.service) })
|
||
return
|
||
}
|
||
mode.value = 'list'
|
||
}
|
||
function serviceListRouteName(service: ServiceType) {
|
||
return ({ labeling: 'overseas-labeling', transfer: 'overseas-storage-transfer', disposal: 'overseas-cargo-disposal', relabel: 'overseas-return-relabel' } as Record<ServiceType, string>)[service]
|
||
}
|
||
async function goToServiceList() {
|
||
if (props.createPage || props.detailPage) {
|
||
await router.push({ name: serviceListRouteName(props.service) })
|
||
return
|
||
}
|
||
mode.value = 'list'
|
||
await loadPage()
|
||
}
|
||
|
||
async function openEdit(transferNo: string) {
|
||
try {
|
||
resetForm()
|
||
await loadWarehouses()
|
||
const detail = await getTransfer(transferNo) as ServiceDetail
|
||
if (!['DRAFT', 'REJECTED'].includes(detail.status || '')) throw new Error('只有草稿或审核驳回状态的转运单可以编辑')
|
||
editingTransferNo.value = transferNo
|
||
Object.assign(form, {
|
||
warehouse: detail.operatingWarehouseId || '', destination: detail.destinationWarehouseCode || detail.fbaWarehouseCode || '',
|
||
privateDestinationAddress: detail.privateDestinationAddress || '', privateDestinationPostalCode: detail.privateDestinationPostalCode || '',
|
||
privateDestinationContact: detail.privateDestinationContactName || '', privateDestinationPhone: detail.privateDestinationContactPhone || '',
|
||
address: detail.deliveryAddress || '', pickupContact: detail.pickupContactName || '',
|
||
pickupPhone: detail.pickupContactPhone || '', deliveryMethod: detail.deliveryMethod || '',
|
||
estimatedArrivalAt: detail.estimatedArrivalAt || '', estimatedShipAt: detail.estimatedShipAt || '',
|
||
fbaShipmentNo: detail.fbaShipmentNo || '', trackingNo: detail.trackingNo || '', remark: detail.remark || '',
|
||
volumeCbm: Number(detail.volumeCbm ?? 0), relabel: Boolean(detail.needRelabel),
|
||
labelDeadline: detail.labelDeadline || '', relabelContact: detail.relabelContactName || '',
|
||
relabelPhone: detail.relabelContactPhone || '', relabelDeliveryMethod: detail.relabelDeliveryMethod || '',
|
||
})
|
||
transferDestinationType.value = detail.destinationType || (detail.fbaWarehouseCode ? 'FBA' : 'NONE')
|
||
cargoRows.value = (detail.items || []).map((item) => ({
|
||
...createCargoRow(), sku: item.skuCode || '', name: item.productNameZh || '',
|
||
quantity: Number(item.quantity || 0), expectedPackageCount: Number(item.packageCount || 0), shippingMark: item.shippingMark || '',
|
||
customerCartonMarks: item.shippingMark || '',
|
||
generatedCartonMarks: (detail.cartons || []).filter(box => String(box.transferItemId) === String(item.id)).map(box => box.cartonMark),
|
||
}))
|
||
const bindings = detail.relabelBindings || []
|
||
if (bindings.length) {
|
||
const operationType = bindings[0].operationType
|
||
labelingMode.value = operationType === 'SKU_LABEL' ? 'sku' : operationType === 'CARTON_LABEL' ? 'carton' : 'carton_and_sku'
|
||
transferRelabelRows.value = bindings.map((binding) => ({
|
||
...createCargoRow(), sku: binding.originalSku || '', newSkuCode: binding.newSku || '',
|
||
quantity: Number(binding.quantity || 1), customerCartonMarks: binding.customerCartonMark || '',
|
||
fbaCartonMark: binding.fbaCartonMark || '', destinationWarehouseId: binding.destinationWarehouseCode || '',
|
||
remark: binding.remark || '',
|
||
}))
|
||
const destinations = [...new Set(bindings.map((binding) => binding.destinationWarehouseCode).filter(Boolean))]
|
||
transferRelabelDefaultDestination.value = destinations.length === 1 ? destinations[0]! : ''
|
||
}
|
||
if (form.warehouse) {
|
||
try { selectedOperationWarehouse.value = await getMerchantWarehouseDetail(form.warehouse) } catch { selectedOperationWarehouse.value = null }
|
||
}
|
||
// The warehouse selector loads inventory candidates for a new transfer, but
|
||
// edit mode starts with an already populated warehouse and never emits the
|
||
// selector change event. Load the same candidates explicitly so the source
|
||
// inventory dropdown is usable when editing a draft.
|
||
if (props.service === 'transfer' && form.warehouse) {
|
||
await loadTransferInventoryCandidates(form.warehouse)
|
||
const sourceInboundCode = String(detail.inboundCode || '')
|
||
if (transferInventoryOptions.value.some((option) => option.value === sourceInboundCode)) {
|
||
selectedTransferInventory.value = sourceInboundCode
|
||
await loadTransferSourceCartons()
|
||
if (transferDestinationType.value === 'NONE') transferDestinationType.value = 'FBA'
|
||
form.inboundNo = sourceInboundCode
|
||
selectedTransferSourceOrderIds.value = (detail.sourceOrders || [])
|
||
.map((item) => String(item.lastlegOrderId || '')).filter(Boolean)
|
||
}
|
||
}
|
||
mode.value = 'edit'
|
||
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '草稿加载失败') }
|
||
}
|
||
async function selectDiscardWarehouse() {
|
||
discardSelectedBoxKeys.value = []
|
||
discardBoxSearch.value = ''
|
||
selectedDiscardOrderId.value = ''
|
||
form.inboundNo = ''
|
||
discardCandidates.value = []
|
||
cargoRows.value = []
|
||
transferRelabelRows.value = []
|
||
if (!form.warehouse) return
|
||
discardCandidatesLoading.value = true
|
||
try {
|
||
const warehouse = form.warehouse
|
||
const candidates = await listDiscardCandidates()
|
||
if (form.warehouse !== warehouse) return
|
||
discardCandidates.value = candidates
|
||
.filter((candidate) => String(candidate.overseasWarehouseId) === warehouse)
|
||
.filter((candidate) => candidate.sourceType === 'DROPSHIP' ? Boolean(candidate.orderId) : candidate.sourceType === 'RETURN_CARTON' ? Boolean(candidate.returnCartonId) : candidate.sourceType === 'INVENTORY' && Boolean(candidate.inventoryPositionId))
|
||
.map((candidate) => ({ ...candidate, orderId: candidate.sourceType === 'RETURN_CARTON' ? `return:${candidate.returnCartonId}` : candidate.sourceType === 'INVENTORY' ? `inventory:${candidate.inventoryPositionId}` : String(candidate.orderId) }))
|
||
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '可废弃入库单加载失败') }
|
||
finally { discardCandidatesLoading.value = false }
|
||
}
|
||
function createCargoRow(): CargoRow { return { id: ++cargoSequence, sku: '', name: '', quantity: 1, expectedPackageCount: 1, fnsku: '', shipmentNo: '', newSkuCode: '', shippingMark: '', customerCartonMarks: '', cartonMark: '', fbaCartonMark: '', destinationWarehouseId: '', remark: '' } }
|
||
function addCargoRow() { cargoRows.value.push(createCargoRow()) }
|
||
function addTransferRelabelRow() {
|
||
const source = cargoRows.value[Math.min(transferRelabelRows.value.length, Math.max(0, cargoRows.value.length - 1))]
|
||
transferRelabelRows.value.push({ ...createCargoRow(), sku: source?.sku || '' })
|
||
}
|
||
function clearTransferRelabelRows() {
|
||
transferRelabelRows.value = []
|
||
}
|
||
function addSkuRelabelRow() {
|
||
if (isReturnRelabelSource.value) { ElMessage.warning('请从上方实际包裹/箱库存选择 SKU'); return }
|
||
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: '', shipmentNo: '', destinationWarehouseId: form.destination, remark: '' })
|
||
}
|
||
function removeSkuRelabelRow(id: number) { skuRelabelRows.value = skuRelabelRows.value.filter((row) => row.id !== id) }
|
||
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.shipmentNo.trim()) row.shipmentNo = mapping.shipmentNo
|
||
if (!row.destinationWarehouseId) row.destinationWarehouseId = mapping.destinationWarehouseId
|
||
}
|
||
function selectShippingLabel(event: Event) {
|
||
shippingLabelFile.value = (event.target as HTMLInputElement).files?.[0] || null
|
||
if (isLabelingCreate.value) void nextTick(() => labelingFormRef.value?.validateField('shippingLabelFile').catch(() => undefined))
|
||
}
|
||
function selectCartonTotalFile(event: Event) {
|
||
cartonTotalFile.value = (event.target as HTMLInputElement).files?.[0] || null
|
||
if (cartonTotalFile.value && !requiresBothTotalFiles.value) clearCreateTotalFile('SKU_TOTAL')
|
||
}
|
||
function selectSkuTotalFile(event: Event) {
|
||
skuTotalFile.value = (event.target as HTMLInputElement).files?.[0] || null
|
||
if (skuTotalFile.value && !requiresBothTotalFiles.value) clearCreateTotalFile('CARTON_TOTAL')
|
||
}
|
||
function clearCreateTotalFile(scope: 'CARTON_TOTAL' | 'SKU_TOTAL') {
|
||
if (saving.value) return
|
||
const file = scope === 'CARTON_TOTAL' ? cartonTotalFile : skuTotalFile
|
||
const input = scope === 'CARTON_TOTAL' ? cartonTotalCreateInput : skuTotalCreateInput
|
||
file.value = null
|
||
if (input.value) input.value.value = ''
|
||
}
|
||
function chooseCreateTotalFile(scope: 'CARTON_TOTAL' | 'SKU_TOTAL') {
|
||
;(scope === 'CARTON_TOTAL' ? cartonTotalCreateInput : skuTotalCreateInput).value?.click()
|
||
}
|
||
function buildSkuRelabelItems(): RelabelCreate['items'] {
|
||
const groups = new Map<string, SkuRelabelRow[]>()
|
||
for (const row of skuRelabelRows.value) {
|
||
const cartonMark = row.cartonMark.trim().toUpperCase()
|
||
if (!cartonMark) throw new Error('请填写每个箱子的我方箱唛')
|
||
const rows = groups.get(cartonMark) || []
|
||
rows.push(row)
|
||
groups.set(cartonMark, rows)
|
||
}
|
||
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) => {
|
||
const source = isReturnRelabelSource.value
|
||
? cargoRows.value.find(item => item.id === rows[0].sourceRowId && item.cartonMark === rows[0].cartonMark.trim())
|
||
: cargoRows.value.find((item) => item.id === rows[0].sourceRowId) || anchors[groupIndex]
|
||
if (isReturnRelabelSource.value && !source) throw new Error('请选择该退货单实际可用的来源箱号')
|
||
if (isReturnRelabelSource.value) {
|
||
const candidate = selectedReturnCandidates.value.find(item => String(item.returnCartonId) === source?.returnCartonId)
|
||
if (rows.some(row => row.sourceRowId !== source?.id)) throw new Error('同一换标箱内的 SKU 必须选择相同的实际来源包裹/箱')
|
||
const requested = new Map<string, number>()
|
||
for (const row of rows) {
|
||
const quantity = (requested.get(row.originalSku) || 0) + row.quantity
|
||
requested.set(row.originalSku, quantity)
|
||
if (quantity > Number(candidate?.skuItems?.find(item => item.skuCode === row.originalSku)?.quantity || 0)) throw new Error(`SKU ${row.originalSku} 超过来源箱可用数量`)
|
||
}
|
||
}
|
||
if (!source?.returnCartonId && (!source?.sourceInventoryBalanceId || !source.sourceInventoryPositionId || !source.sourceInboundOrderId)) {
|
||
throw new Error('来源库存已变化,请重新选择有库存的历史入仓单')
|
||
}
|
||
const cartonMark = rows[0].cartonMark.trim()
|
||
const changeCarton = labelingMode.value === 'carton_and_sku'
|
||
const fbaCartonMark = rows[0].fbaCartonMark.trim()
|
||
const shipmentNo = rows[0].shipmentNo.trim()
|
||
if (changeCarton && (!fbaCartonMark || !form.destination
|
||
|| rows.some((row) => row.fbaCartonMark.trim() !== fbaCartonMark))) {
|
||
throw new Error('同一我方箱唛的组合换标必须使用相同的新 FBA 箱唛,并选择整票 FBA 目的仓')
|
||
}
|
||
if (changeCarton && (!shipmentNo || rows.some(row => row.shipmentNo.trim() !== shipmentNo))) {
|
||
throw new Error('同一我方箱唛的组合换标必须填写相同的 FBA 货件编号')
|
||
}
|
||
return {
|
||
sourceInventoryBalanceId: source.sourceInventoryBalanceId,
|
||
sourceInventoryPositionId: source.sourceInventoryPositionId,
|
||
sourceInboundOrderId: source.sourceInboundOrderId,
|
||
returnCartonId: source.returnCartonId,
|
||
skuCode: source.sku || rows[0].originalSku,
|
||
productNameZh: source.name,
|
||
totalPackageCount: 1,
|
||
notifiedPackageCount: 1,
|
||
shipmentNo: changeCarton ? shipmentNo : source.shipmentNo || cartonMark,
|
||
trackingCode: form.trackingNo || undefined,
|
||
newSkuCode: rows[0].newSku,
|
||
labelRequirement: form.requirement || undefined,
|
||
cartonMark,
|
||
fbaCartonMark: changeCarton ? fbaCartonMark : undefined,
|
||
destinationFbaWarehouseCode: changeCarton ? form.destination : 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 }
|
||
const source = cargoRows.value.find((row) => !row.included)
|
||
if (!source) { ElMessage.warning('所选入仓单没有更多可添加的库存箱'); return }
|
||
source.included = true
|
||
}
|
||
function removeCargoRow(id: number) {
|
||
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
|
||
}
|
||
cargoRows.value = cargoRows.value.filter((row) => row.id !== id)
|
||
}
|
||
function clearRelabelRows() {
|
||
for (const row of cargoRows.value) Object.assign(row, { included: false, shipmentNo: '', cartonMark: '', fbaCartonMark: '', destinationWarehouseId: '', remark: '' })
|
||
}
|
||
function csvCell(value: string | number | undefined) { return `"${String(value ?? '').replaceAll('"', '""')}"` }
|
||
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: Number(row.expectedPackageCount || 0) }, () => [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]).map((name) => name === '仓库唛头' || name === '旧唛头' ? '旧箱唛' : name)
|
||
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} 个客户箱唛`)
|
||
}
|
||
function relabelTemplateTypeForMode(mode: LabelingMode): RelabelTemplateType {
|
||
return mode === 'carton' ? 'carton' : mode === 'sku' ? 'sku' : 'mixed'
|
||
}
|
||
function relabelTemplateTypeLabel(type: RelabelTemplateType) {
|
||
return type === 'carton' ? '只换箱唛' : type === 'sku' ? '只换 SKU 标签' : 'SKU 和箱唛都换'
|
||
}
|
||
function parseRelabelTemplateType(value: string): RelabelTemplateType | null {
|
||
const normalized = value.trim().toUpperCase().replaceAll(' ', '')
|
||
if (['只换箱唛', '箱号关联', '箱唛换标', 'CARTON', 'CARTON_LABEL'].includes(normalized)) return 'carton'
|
||
if (['只换SKU标签', 'SKU关联', 'SKU换标', 'SKU', 'SKU_LABEL'].includes(normalized)) return 'sku'
|
||
if (['SKU和箱唛都换', 'SKU混箱', 'SKU混箱换标', '混箱', 'MIXED', 'CARTON_AND_SKU'].includes(normalized)) return 'mixed'
|
||
return null
|
||
}
|
||
function downloadRelabelTemplate() {
|
||
if (!cargoRows.value.length) { ElMessage.warning('请先选择有库存的历史入仓单'); return }
|
||
const templateType = relabelTemplateTypeForMode(labelingMode.value)
|
||
const cartonMarkHeader = props.service === 'transfer' ? '客户箱唛号' : '仓库唛头'
|
||
const header = ['明细行标识', '贴标类型', cartonMarkHeader, '旧SKU标签', '新SKU标签', '换标数量(件)', '新FBA箱唛', 'FBA货件编号', '备注']
|
||
const rows = cargoRows.value.map((item) => [item.id, relabelTemplateTypeLabel(templateType), props.service === 'transfer' ? item.customerCartonMarks : item.cartonMark, item.sku, '', 1, '', item.shipmentNo, item.remark])
|
||
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)
|
||
}
|
||
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
|
||
}
|
||
async function importRelabelTemplate(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]).map((name) => name === '仓库唛头' || name === '客户箱唛号' || name === '旧唛头' ? '旧箱唛' : name)
|
||
const requiredHeaders = ['明细行标识', '贴标类型', '旧箱唛', '旧SKU标签', '新SKU标签', '换标数量(件)', '新FBA箱唛', 'FBA货件编号', '备注']
|
||
if (requiredHeaders.some((name) => !headers.includes(name))) { ElMessage.error('统一明细关联模板格式不正确,请重新下载模板'); return }
|
||
const imported = lines.slice(1).map(parseCsvLine).map((values) => Object.fromEntries(headers.map((key, index) => [key, values[index] || '']))).filter((item) => item['明细行标识'] || item['贴标类型'] || item['旧箱唛'] || item['新SKU标签'] || item['新FBA箱唛'])
|
||
if (!imported.length) { ElMessage.error('模板中没有可导入的明细'); return }
|
||
const rows: ImportedRelabelBinding[] = []; let detectedType: RelabelTemplateType | null = null
|
||
for (const item of imported) {
|
||
const templateType = parseRelabelTemplateType(item['贴标类型']); if (!templateType) { ElMessage.error(`明细行 ${item['明细行标识']} 的贴标类型无效`); return }
|
||
if (detectedType && detectedType !== templateType) { ElMessage.error('同一份明细文件只能包含一种贴标类型,请拆分后分别提交换标单'); return }
|
||
detectedType = templateType
|
||
const source = cargoRows.value.find((row) => String(row.id) === item['明细行标识']); if (!source) { ElMessage.error(`明细行 ${item['明细行标识']} 不属于当前库存,请重新下载模板`); return }
|
||
const quantity = Number(item['换标数量(件)']); const requiresSku = templateType !== 'carton'; const requiresCarton = templateType !== 'sku'
|
||
if (!item['旧箱唛'] || (requiresSku && (!item['旧SKU标签'] || !item['新SKU标签'])) || !Number.isInteger(quantity) || quantity < 1 || (requiresCarton && (!item['新FBA箱唛'] || !item['FBA货件编号']))) { ElMessage.error(`明细行 ${item['明细行标识']} 未按“${relabelTemplateTypeLabel(templateType)}”填写完整`); return }
|
||
rows.push({ id: ++skuRelabelSequence, sourceRowId: source.id, cartonMark: item['旧箱唛'].trim(), originalSku: (item['旧SKU标签'] || source.sku).trim(), newSku: item['新SKU标签'].trim(), quantity: templateType === 'carton' ? 1 : quantity, fbaCartonMark: item['新FBA箱唛'].trim(), destinationWarehouseId: form.destination, remark: item['备注'] || '', templateType, selected: true, shipmentNo: item['FBA货件编号'].trim() })
|
||
}
|
||
const keys = rows.map((row) => detectedType === 'carton' ? String(row.sourceRowId) : `${row.cartonMark.toUpperCase()}|${row.originalSku.toUpperCase()}`)
|
||
if (new Set(keys).size !== keys.length) { ElMessage.error('存在重复箱明细或重复的箱内 SKU,请合并后重新上传'); return }
|
||
const cartonTargets = new Map<string, string>()
|
||
for (const row of rows) {
|
||
const key = row.cartonMark.toUpperCase()
|
||
const target = `${row.fbaCartonMark}|${row.shipmentNo}`
|
||
if (detectedType === 'mixed' && cartonTargets.has(key) && cartonTargets.get(key) !== target) { ElMessage.error('同一箱内的 SKU 必须填写相同的新 FBA 箱唛和货件编号'); return }
|
||
cartonTargets.set(key, target)
|
||
}
|
||
importedRelabelBindings.value = rows; ElMessage.success(`已读取 ${rows.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 (props.service === 'transfer') {
|
||
const importedRows = selected.map((binding) => ({
|
||
...createCargoRow(),
|
||
sku: binding.originalSku,
|
||
newSkuCode: binding.newSku,
|
||
quantity: binding.quantity,
|
||
customerCartonMarks: binding.cartonMark,
|
||
fbaCartonMark: binding.fbaCartonMark,
|
||
destinationWarehouseId: '',
|
||
remark: binding.remark,
|
||
}))
|
||
const importedMarks = new Set(importedRows.flatMap(transferCustomerMarks).map((mark) => mark.toUpperCase()))
|
||
transferRelabelRows.value = [
|
||
...transferRelabelRows.value.filter((row) => !transferCustomerMarks(row).some((mark) => importedMarks.has(mark.toUpperCase()))),
|
||
...importedRows,
|
||
]
|
||
labelingMode.value = templateType === 'carton' ? 'carton' : templateType === 'sku' ? 'sku' : 'carton_and_sku'
|
||
importedRelabelBindings.value = importedRelabelBindings.value.filter((item) => !item.selected)
|
||
ElMessage.success(`已导入 ${importedRows.length} 条仓储转运换标明细`)
|
||
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: form.destination, remark: binding.remark })
|
||
}
|
||
labelingMode.value = 'carton'
|
||
} else {
|
||
const importedRows = selected.map(({ templateType: _, selected: __, ...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()))
|
||
item.destinationWarehouseId = form.destination
|
||
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: form.destination, shipmentNo })
|
||
}
|
||
}
|
||
labelingMode.value = templateType === 'mixed' ? 'carton_and_sku' : 'sku'
|
||
}
|
||
importedRelabelBindings.value = importedRelabelBindings.value.filter((item) => !item.selected)
|
||
ElMessage.success(`已将 ${selected.length} 条绑定关系导入本次换标明细`)
|
||
}
|
||
function selectDiscardInbound(orderId: string) {
|
||
discardRequestedPackages.value = undefined
|
||
discardSelectedBoxKeys.value = []
|
||
discardBoxSearch.value = ''
|
||
const candidate = discardDocuments.value.find((item) => item.orderId === orderId)
|
||
if (!candidate) { cargoRows.value = []; return }
|
||
form.inboundNo = candidate.inboundCode || candidate.inboundOrderNo
|
||
form.warehouse = candidate.overseasWarehouseId
|
||
if (candidate.cartonSelectionSupported) { selectAllDiscardBoxes(true); return }
|
||
cargoRows.value = candidate.sources.flatMap(source => source.skus.map((sku) => ({
|
||
id: ++cargoSequence, sku: sku.skuCode, name: sku.productName, quantity: sku.availablePackages,
|
||
availableQuantity: sku.availablePackages, sourceOrderId: source.orderId, sourceType: source.sourceType,
|
||
returnCartonId: source.returnCartonId, sourceInventoryPositionId: source.inventoryPositionId, palletNo: source.palletNo, cartonMark: source.cartonMark || '',
|
||
fnsku: '', shipmentNo: '', newSkuCode: '', shippingMark: '', customerCartonMarks: '', fbaCartonMark: '', destinationWarehouseId: '', remark: '',
|
||
})))
|
||
}
|
||
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)])) }
|
||
function warehouseDisplay(value?: string) {
|
||
if (!value) return '—'
|
||
return value.split(',').map((code) => {
|
||
const warehouse = [...operationWarehouses.value, ...fbaWarehouses.value].find((item) => String(item.id) === code || item.warehouseCode === code)
|
||
return warehouse ? warehouseLabel(warehouse) : '—'
|
||
}).join('、')
|
||
}
|
||
function statusLabel(value: string) {
|
||
if (props.service === 'labeling') {
|
||
const relabelLabels: Record<string, string> = {
|
||
WAIT_APPROVAL: '待审核', PENDING_REVIEW: '待审核', PENDING_PROCESSING: '待仓库处理', PROCESSING: '处理中',
|
||
CUSTOMER_CONFIRM_PENDING: '待客户确认', READY_FOR_OUTBOUND: '换标完成', COMPLETED: '已完成',
|
||
REJECTED: '已驳回', CANCELLED: '已取消',
|
||
}
|
||
return relabelLabels[value] || value || '—'
|
||
}
|
||
if (props.service === 'transfer') {
|
||
const transferLabels: Record<string, string> = {
|
||
DRAFT: '草稿', PENDING_REVIEW: '待审核', APPROVED: '审核通过', REJECTED: '审核驳回',
|
||
WAIT_PDA_RECEIVING: '待PDA收货', INBOUND_MEASURING: '入库测量中',
|
||
WAIT_CUSTOMER_CONFIRMATION: '待客户确认实测', WAIT_RELABEL: '待换标',
|
||
WAIT_FBA_DESTINATION: '待确认目的地', READY_FOR_DISPATCH: '待排车',
|
||
WAIT_OUTBOUND: '待出库', PENDING_ASSIGNMENT: '待分配', ALLOCATED: '已分配库存',
|
||
OUTBOUNDED: '已出库', PARTIALLY_RECEIVED: '部分收货', RECEIVED: '已收货',
|
||
INBOUND_COMPLETED: '入库完成', COMPLETED: '已完成',
|
||
}
|
||
if (transferLabels[value]) return transferLabels[value]
|
||
}
|
||
const labels: Record<string, string> = {
|
||
|
||
PENDING_PROCESSING: '待处理', PROCESSING: '处理中', CUSTOMER_CONFIRM_PENDING: '待客户确认',
|
||
READY_FOR_OUTBOUND: '换标完成', COMPLETED: '已完成', CANCELLED: '已取消',
|
||
DRAFT: '草稿', PENDING_REVIEW: '待审核', APPROVED: '待 PDA 废弃', PDA_COMPLETED: '待客户确认', REJECTED: '已拒绝', PENDING_ASSIGNMENT: '待分配',
|
||
WAIT_PDA_RECEIVING: '待PDA收货', WAIT_CUSTOMER_CONFIRMATION: '待客户确认实测', WAIT_FBA_DESTINATION: '待确认目的地', WAIT_RELABEL: '待换标', READY_FOR_DISPATCH: '待排车', WAIT_OUTBOUND: '待出库',
|
||
ALLOCATED: '已分配', OUTBOUNDED: '已出库', PARTIALLY_RECEIVED: '部分收货', RECEIVED: '已收货', INBOUND_COMPLETED: '入库完成', PENDING_DELIVERY: '待派送', DELIVERING: '派送中', DELIVERED: '派送完成',
|
||
|
||
}
|
||
return labels[value] || value || '—'
|
||
}
|
||
type StatusTagType = 'primary' | 'success' | 'warning' | 'danger' | 'info'
|
||
function labelingTypeLabels(value: unknown): string[] {
|
||
return typeof value === 'string' && value !== '—' ? [...new Set(value.split('、'))] : []
|
||
}
|
||
function labelingTypeTagType(label: string): StatusTagType {
|
||
if (label === '只换 SKU') return 'primary'
|
||
if (label === '只换箱唛') return 'warning'
|
||
return 'success'
|
||
}
|
||
function labelingStatusTagType(status: string): StatusTagType {
|
||
if (status === 'CANCELLED') return 'info'
|
||
if (!['READY_FOR_OUTBOUND', 'COMPLETED', 'REJECTED', 'WAIT_APPROVAL', 'PENDING_REVIEW', 'PENDING_PROCESSING', 'CUSTOMER_CONFIRM_PENDING', 'PROCESSING'].includes(status)) return 'info'
|
||
return relabelStatusTagType(status)
|
||
}
|
||
function relabelStatusTagType(status: string): StatusTagType {
|
||
if (['READY_FOR_OUTBOUND', 'COMPLETED'].includes(status)) return 'success'
|
||
if (['REJECTED', 'CANCELLED'].includes(status)) return 'danger'
|
||
if (['WAIT_APPROVAL', 'PENDING_REVIEW', 'PENDING_PROCESSING', 'CUSTOMER_CONFIRM_PENDING'].includes(status)) return 'warning'
|
||
return 'primary'
|
||
}
|
||
function relabelFulfillmentLabel(value?: string) {
|
||
const labels: Record<string, string> = {
|
||
WAIT_APPROVAL: '待审核', WAIT_PROCESSING: '待仓库处理',
|
||
WAIT_RELABEL: '待换标', WAIT_PRE_DISPATCH: '待排车', READY_FOR_DISPATCH: '待排车',
|
||
OUTBOUND_CREATED: '换标出库单已创建', OUTBOUNDED: '已出库', COMPLETED: '已完成',
|
||
}
|
||
return labels[value || ''] || value || '—'
|
||
}
|
||
function relabelFulfillmentTagType(status?: string): StatusTagType {
|
||
if (['OUTBOUNDED', 'COMPLETED'].includes(status || '')) return 'success'
|
||
if (['WAIT_APPROVAL', 'WAIT_PROCESSING', 'WAIT_RELABEL', 'WAIT_PRE_DISPATCH', 'READY_FOR_DISPATCH'].includes(status || '')) return 'warning'
|
||
return 'primary'
|
||
}
|
||
function transferStatusTagType(status: string): StatusTagType {
|
||
if (['RECEIVED', 'COMPLETED'].includes(status)) return 'success'
|
||
if (['REJECTED', 'CANCELLED'].includes(status)) return 'danger'
|
||
if (['DRAFT'].includes(status)) return 'info'
|
||
if (['PENDING_REVIEW', 'WAIT_CUSTOMER_CONFIRMATION', 'WAIT_RELABEL', 'WAIT_FBA_DESTINATION', 'READY_FOR_DISPATCH', 'WAIT_OUTBOUND', 'PARTIALLY_RECEIVED'].includes(status)) return 'warning'
|
||
return 'primary'
|
||
}
|
||
function transferRelabelStatus(item: { needRelabel?: boolean; relabelNo?: string; relabelStatus?: string; relabelFulfillmentStatus?: string }) {
|
||
if (!item.needRelabel) return { code: 'NOT_REQUIRED', label: '无需换标' }
|
||
if (!item.relabelNo) return { code: 'WAIT_RELABEL', label: '待换标' }
|
||
const code = item.relabelStatus || 'PENDING_PROCESSING'
|
||
const labels: Record<string, string> = {
|
||
PENDING_REVIEW: '待审核', PENDING_PROCESSING: '待处理', PROCESSING: '换标中',
|
||
CUSTOMER_CONFIRM_PENDING: '待客户确认', READY_FOR_OUTBOUND: '已换标', COMPLETED: '已换标',
|
||
REJECTED: '已驳回', CANCELLED: '已取消',
|
||
}
|
||
const fulfillmentLabels: Record<string, string> = {
|
||
WAIT_RELABEL: '待换标', WAIT_CUSTOMER_CONFIRMATION: '待客户确认', READY_FOR_DISPATCH: '已换标',
|
||
OUTBOUND_CREATED: '已换标', OUTBOUNDED: '已换标', NOT_REQUIRED: '无需换标',
|
||
}
|
||
return { code, label: labels[code] || fulfillmentLabels[item.relabelFulfillmentStatus || ''] || statusLabel(code) }
|
||
}
|
||
function transferRelabelStatusTagType(status: string): StatusTagType {
|
||
if (['READY_FOR_OUTBOUND', 'COMPLETED'].includes(status)) return 'success'
|
||
if (['REJECTED', 'CANCELLED'].includes(status)) return 'danger'
|
||
if (status === 'NOT_REQUIRED') return 'info'
|
||
if (['WAIT_RELABEL', 'PENDING_REVIEW', 'CUSTOMER_CONFIRM_PENDING'].includes(status)) return 'warning'
|
||
return 'primary'
|
||
}
|
||
function deliveryMethodLabel(value: string) {
|
||
const labels: Record<string, string> = {
|
||
TRUCK: '卡车派送', EXPRESS: '快递派送', AMAZON_PICKUP: '亚马逊自提', CUSTOMER_PICKUP: '客户自提',
|
||
INVENTORY_TRANSFER: '库存转运', WAREHOUSE_PICKUP: '仓库上门提货', CUSTOMER_DELIVERY: '客户送货到仓',
|
||
// Keep the legacy value readable for historical applications created before the four-way split.
|
||
PICKUP: '仓库自提',
|
||
}
|
||
return labels[value] || value || '—'
|
||
}
|
||
function destinationTypeLabel(value?: string, fbaWarehouseCode?: string) {
|
||
if (value === 'PRIVATE') return '私人仓'
|
||
if (value === 'FBA' || fbaWarehouseCode) return 'FBA 仓'
|
||
return '暂未确定'
|
||
}
|
||
function formatDateTime(value?: string | null) {
|
||
return value ? value.replace('T', ' ').slice(0, 19) : '—'
|
||
}
|
||
let discardLoadGeneration = 0
|
||
async function load() {
|
||
const discardRequest = props.service === 'disposal' ? ++discardLoadGeneration : undefined
|
||
loading.value = true
|
||
try {
|
||
if (props.service === 'labeling') {
|
||
const query = { page: page.value, size: pageSize.value, relabelNo: filters.number || undefined, overseasWarehouseId: filters.warehouse || undefined, status: filters.status || undefined, createdFrom: dateTime(filters.start), createdTo: dateTime(filters.end, true), reviewedFrom: dateTime(filters.reviewStart), reviewedTo: dateTime(filters.reviewEnd, true), completedFrom: dateTime(filters.completedStart), completedTo: dateTime(filters.completedEnd, true), outboundFrom: dateTime(filters.outboundStart), outboundTo: dateTime(filters.outboundEnd, true) }
|
||
const [result, dimensionResult] = await Promise.all([listRelabels(query), getRelabelQuantities(query).catch(() => null)])
|
||
total.value = Number.isFinite(Number(result.total)) ? Number(result.total) : result.records.length
|
||
submittedRows.value = result.records.map((item) => {
|
||
const destinationWarehouses = warehouseDisplay(item.overseasWarehouseId)
|
||
return { ...row([item.relabelNo, (item.relatedBusinessOrderNos || []).join('、') || '—', destinationWarehouses, dimensionResult?.orders[item.relabelNo]?.skuCount ?? '—', dimensionResult?.orders[item.relabelNo]?.cartonCount ?? '—', dimensionResult?.orders[item.relabelNo]?.labelPieceCount ?? '—', (item.operationTypes || []).map(type => ({ CARTON_LABEL: '只换箱唛', SKU_LABEL: '只换 SKU', RETURN_BULK_SKU_LABEL: '只换 SKU', CARTON_AND_SKU: 'SKU 和箱唛都换' }[type] || type)).join('、') || '—', statusLabel(item.status), formatDateTime(item.createdAt)]), __status: item.status, __confirmedAt: item.customerConfirmedAt || '' }
|
||
})
|
||
} else if (props.service === 'disposal') {
|
||
const discardQuery = { page: page.value, size: pageSize.value, discardNo: filters.number || undefined, inboundCode: filters.inboundNo || undefined, overseasWarehouseId: filters.warehouse || undefined, status: filters.status || undefined, createdFrom: dateTime(filters.start), createdTo: dateTime(filters.end, true), completedFrom: dateTime(filters.completedStart), completedTo: dateTime(filters.completedEnd, true) }
|
||
const result = await listDiscards(discardQuery)
|
||
if (discardRequest !== discardLoadGeneration || props.service !== 'disposal') return
|
||
total.value = Number.isFinite(Number(result.total)) ? Number(result.total) : result.records.length
|
||
submittedRows.value = result.records.map((item) => ({ ...row([item.discardNo, item.outboundNo || '—', item.inboundCodes, item.overseasWarehouseName || warehouseDisplay(item.overseasWarehouseId) || '—', item.skuCodes?.length ? `${item.skuCount} 种|${item.skuCodes.join('、')}` : '未记录', item.requestedPackages, `${item.currency} ${item.totalFee}`, statusLabel(item.status), formatDateTime(item.customerConfirmedAt), formatDateTime(item.createdAt)]), __status: item.status }))
|
||
} else {
|
||
const query = { page: page.value, size: pageSize.value, transferNo: filters.number || undefined, inboundCode: filters.inboundNo || undefined, operatingWarehouseId: filters.warehouse || undefined, fbaWarehouseCode: filters.destination || undefined, status: filters.status || undefined, ...(props.service === 'relabel' ? { needRelabel: true } : {}), createdFrom: dateTime(filters.start), createdTo: dateTime(filters.end, true), reviewedFrom: dateTime(filters.reviewStart), reviewedTo: dateTime(filters.reviewEnd, true), measuredFrom: props.service === 'relabel' ? dateTime(filters.measuredStart) : dateTime(filters.completedStart), measuredTo: props.service === 'relabel' ? dateTime(filters.measuredEnd, true) : dateTime(filters.completedEnd, true), outboundFrom: dateTime(filters.outboundStart), outboundTo: dateTime(filters.outboundEnd, true) }
|
||
const result = await listTransfers(query)
|
||
total.value = Number.isFinite(Number(result.total)) ? Number(result.total) : result.records.length
|
||
submittedRows.value = result.records.map((item) => {
|
||
const operationWarehouse = warehouseDisplay(item.operatingWarehouseId)
|
||
const relabelStatus = transferRelabelStatus(item)
|
||
return props.service === 'transfer'
|
||
? { ...row([item.transferNo, item.packageCount, operationWarehouse, item.fbaWarehouseCode, deliveryMethodLabel(item.deliveryMethod), item.needRelabel ? '是' : '否', item.relabelNo, relabelStatus.label, statusLabel(item.status), formatDateTime(item.createdAt)]), __status: item.status, __statusLabel: statusLabel(item.status), __relabelStatus: relabelStatus.code, __relabelStatusLabel: relabelStatus.label }
|
||
: row([item.transferNo, '—', item.fbaWarehouseCode, '—', item.packageCount, '是', item.relabelNo ? '已生成' : '待生成', statusLabel(item.status), formatDateTime(item.reviewedAt), formatDateTime(item.measuredAt), formatDateTime(item.outboundAt), formatDateTime(item.createdAt)])
|
||
})
|
||
}
|
||
} catch (error) { if (discardRequest !== undefined && discardRequest !== discardLoadGeneration) return; submittedRows.value = []; total.value = 0; ElMessage.error(error instanceof Error ? error.message : '列表加载失败') }
|
||
finally { if (discardRequest === undefined || discardRequest === discardLoadGeneration) loading.value = false }
|
||
}
|
||
function query() { if (page.value === 1) void load(); else page.value = 1 }
|
||
function resetListQuery() { resetFilters(); query() }
|
||
function listRowActions(candidate: object): TableAction[] {
|
||
const item = candidate as Record<string, string>
|
||
const actions: TableAction[] = [{ key: 'detail', label: '详情', run: () => openDetail(item) }]
|
||
if (canCreate.value && props.service === 'transfer' && ['DRAFT', 'REJECTED'].includes(item.__status)) actions.push({ key: 'edit', label: '编辑', run: () => openEdit(item['0']) })
|
||
if (canConfirmTransfer.value && props.service === 'transfer' && item.__status === 'WAIT_CUSTOMER_CONFIRMATION') actions.push({ key: 'confirm-measurement', label: '确认测量', run: () => confirmTransferFromList(item['0']) })
|
||
if (canConfirmRelabel.value && props.service === 'labeling' && item.__status === 'CUSTOMER_CONFIRM_PENDING' && !item.__confirmedAt) actions.push({ key: 'confirm-relabel', label: '确认换标', run: () => confirmRelabelFromList(item['0']) })
|
||
if (canConfirmDiscard.value && props.service === 'disposal' && item.__status === 'PDA_COMPLETED') actions.push({ key: 'confirm-discard', label: '废弃确认', run: () => openCompletionConfirmation(item['0'], 'disposal') })
|
||
return actions
|
||
}
|
||
watch([page, pageSize], ([nextPage, nextSize], [previousPage, previousSize]) => {
|
||
if (nextSize !== previousSize && nextPage !== 1) { page.value = 1; return }
|
||
void load()
|
||
})
|
||
function printForecastCartons() {
|
||
if (saving.value) return
|
||
let html: string
|
||
try { html = prepareTransferCartonLabels(cargoRows.value) }
|
||
catch (error) { ElMessage.warning(error instanceof Error ? error.message : '箱唛生成失败'); return }
|
||
const printWindow = window.open('', '_blank')
|
||
if (!printWindow) { ElMessage.warning('请允许浏览器打开打印窗口'); return }
|
||
printWindow.opener = null
|
||
printWindow.document.open(); printWindow.document.write(html); printWindow.document.close()
|
||
printWindow.focus(); printWindow.print()
|
||
}
|
||
async function submit(action: 'draft' | 'submit' = 'submit') {
|
||
if (saving.value) return
|
||
if (isLabelingCreate.value) {
|
||
orderInfoCollapsed.value = false
|
||
await nextTick()
|
||
if (!await labelingFormRef.value?.validate().catch(() => false)) return
|
||
}
|
||
if (props.service === 'transfer' && inventoryTransfer.value && action === 'draft') {
|
||
ElMessage.warning('选择库存来源后不能保存草稿,请直接提交订单')
|
||
return
|
||
}
|
||
if (isDisposalCreate.value && !await labelingFormRef.value?.validate().catch(() => false)) return
|
||
if (transferRelabelRequired.value) form.relabel = true
|
||
if (isTransferCreate.value) {
|
||
await nextTick()
|
||
if (!await labelingFormRef.value?.validate().catch(() => false)) return
|
||
}
|
||
const primaryWarehouse = props.service === 'relabel' ? form.sourceWarehouse : form.warehouse
|
||
const relabelRows = labelingMode.value === 'carton' ? cartonDetailRows.value : skuRelabelRows.value
|
||
const invalidCargo = props.service === 'disposal'
|
||
? !selectedDiscardOrderId.value || (discardQuantityMode.value && (!Number.isInteger(discardRequestedPackages.value) || Number(discardRequestedPackages.value) < 1 || Number(discardRequestedPackages.value) > Number(discardSelectedCandidate.value?.availablePackages || 0))) || cargoRows.value.every((item) => item.quantity < 1)
|
||
|| cargoRows.value.some((item) => item.quantity < 0 || item.quantity > Number(item.availableQuantity || 0))
|
||
: (props.service === 'labeling' || props.service === 'relabel') && (relabelRows.length === 0 || (props.service === 'labeling' && labelingMode.value !== 'carton'
|
||
? skuRelabelRows.value.some((item) => !item.cartonMark || !item.originalSku || !item.newSku || item.quantity < 1
|
||
|| (labelingMode.value === 'carton_and_sku' && (!item.fbaCartonMark || !item.shipmentNo.trim())))
|
||
: (props.service === 'labeling' ? cartonDetailRows.value : cargoRows.value).some((item) => (props.service !== 'labeling' && !item.sku) || !item.name
|
||
|| (props.service !== 'labeling' && item.quantity < 1))))
|
||
if ((props.service === 'relabel' && !form.inboundNo) || !primaryWarehouse || invalidCargo) {
|
||
ElMessage.warning('请填写必填信息和完整的货物明细'); return
|
||
}
|
||
if (props.service === 'labeling' && (!form.labelDeadline || !form.contact || !form.phone)) {
|
||
ElMessage.warning('请填写贴标截止时间和海外仓联系人信息'); return
|
||
}
|
||
if (props.service === 'labeling' && requiresBothTotalFiles.value && (!cartonTotalFile.value || !skuTotalFile.value)) {
|
||
ElMessage.warning('SKU 和箱唛都换时,必须同时上传箱唛总文件和 SKU 总文件'); return
|
||
}
|
||
if (props.service === 'labeling' && !requiresBothTotalFiles.value && (labelingMode.value === 'sku' ? !skuTotalFile.value : !cartonTotalFile.value)) {
|
||
ElMessage.warning(labelingMode.value === 'sku' ? '请上传 SKU 标签总文件' : '请上传箱唛总文件'); return
|
||
}
|
||
if (props.service === 'labeling' && labelingMode.value !== 'sku' && !form.deliveryMethod) {
|
||
ElMessage.warning('请选择换标完成后的派送方式'); return
|
||
}
|
||
if (props.service === 'labeling' && labelingMode.value !== 'sku' && !form.destination) {
|
||
ElMessage.warning('请选择本张贴标申请统一使用的 FBA 目的仓'); return
|
||
}
|
||
if (props.service === 'labeling' && labelingMode.value !== 'sku' && form.deliveryMethod === 'EXPRESS'
|
||
&& form.relabel && !shippingLabelFile.value) {
|
||
ElMessage.warning('选择打印面单时必须上传面单文件'); return
|
||
}
|
||
if (props.service === 'labeling' && labelingMode.value === 'carton' && cartonDetailRows.value.some((item) => !item.shipmentNo || !item.cartonMark || !item.fbaCartonMark)) {
|
||
ElMessage.warning('请上传关联面单,确保每箱的我方箱唛和 FBA 箱唛完整'); return
|
||
}
|
||
if (props.service === 'labeling' && labelingMode.value === 'carton' && cartonDetailRows.value.some((item) => !item.returnCartonId && !item.sourceInventoryPositionId)) {
|
||
ElMessage.warning('请选择有库存的历史入仓单'); return
|
||
}
|
||
if (props.service === 'disposal' && !form.reason) {
|
||
ElMessage.warning('请选择废弃原因'); return
|
||
}
|
||
if (props.service === 'disposal' && discardQuantityMode.value && !form.remark.trim()) {
|
||
ElMessage.warning('请填写废弃备注'); return
|
||
}
|
||
if (props.service === 'relabel' && (!form.destination || !form.address || !form.deliveryMethod || !form.estimatedArrivalAt || !form.fbaShipmentNo || form.volumeCbm <= 0)) {
|
||
ElMessage.warning('请填写完整的派送信息、体积、预计到达时间和 FBA 货件号'); return
|
||
}
|
||
if (props.service === 'transfer' && !inventoryTransfer.value && (!form.warehouse || !form.deliveryMethod || !form.address || !form.estimatedArrivalAt || (form.deliveryMethod === 'WAREHOUSE_PICKUP' && (!form.pickupContact || !form.pickupPhone)))) {
|
||
const missing = [!form.warehouse && '海外仓', !form.deliveryMethod && '送货方式', !form.address && '地址', form.deliveryMethod === 'WAREHOUSE_PICKUP' && !form.pickupContact && '提货联系人', form.deliveryMethod === 'WAREHOUSE_PICKUP' && !form.pickupPhone && '联系人电话', !form.estimatedArrivalAt && '预计入仓时间'].filter(Boolean).join('、')
|
||
ElMessage.warning(`请填写:${missing || '必填信息'}`); return
|
||
}
|
||
if (props.service === 'transfer' && (!cargoRows.value.length || cargoRows.value.some((row) =>
|
||
!Number.isSafeInteger(row.quantity) || row.quantity < 1 || !Number.isSafeInteger(row.expectedPackageCount) || Number(row.expectedPackageCount) < 1))) {
|
||
ElMessage.warning('请填写每个 SKU 的数量(件)和预计箱数,均须为正整数'); return
|
||
}
|
||
if (inventoryTransfer.value && (transferSourceLoading.value || transferSourceError.value)) { ElMessage.warning('请等待原单箱明细加载成功后再保存'); return }
|
||
if (inventoryTransfer.value && transferDestinationType.value === 'NONE') { ElMessage.warning('库存转运请选择 FBA 仓或私人仓'); return }
|
||
if (props.service === 'transfer' && transferDestinationType.value === 'FBA' && !form.destination) {
|
||
ElMessage.warning('请选择 FBA 目的仓'); return
|
||
}
|
||
if (props.service === 'transfer' && transferDestinationType.value === 'PRIVATE' && (!form.privateDestinationAddress || !form.privateDestinationPostalCode || !form.privateDestinationContact || !form.privateDestinationPhone)) {
|
||
ElMessage.warning('请完整填写私人仓地址、邮编、联系人和电话'); return
|
||
}
|
||
if (props.service === 'transfer' && form.relabel) {
|
||
if (!form.labelDeadline || !form.relabelContact || !form.relabelPhone || (labelingMode.value !== 'sku' && !form.relabelDeliveryMethod)) { ElMessage.warning('请填写换标截止时间、换标联系人和换标后派送方式'); return }
|
||
if (!transferRelabelDefaultDestination.value) { ElMessage.warning('请选择整票换标货物的 FBA 目的仓'); return }
|
||
if (action === 'submit' && requiresBothTotalFiles.value && (!cartonTotalFile.value || !skuTotalFile.value)) { ElMessage.warning('SKU 和箱唛都换时,必须同时上传箱唛总文件和 SKU 总文件'); return }
|
||
if (action === 'submit' && !requiresBothTotalFiles.value && !cartonTotalFile.value && !skuTotalFile.value) { ElMessage.warning('请上传箱唛总文件或 SKU 总文件(二选一)'); return }
|
||
const relabelRows = transferRelabelRows.value
|
||
const allMarks = relabelRows.flatMap(transferCustomerMarks)
|
||
if (inventoryTransfer.value && allMarks.some(mark => !transferSourceCartons.value.some(box => box.cartonMark === mark))) {
|
||
ElMessage.error('当前箱子不存在或不属于当前订单'); return
|
||
}
|
||
if (!relabelRows.length || relabelRows.some((row) => transferCustomerMarks(row).length !== row.quantity)) { ElMessage.warning('每个换标明细的客户箱唛数量必须与换标数量一致'); return }
|
||
if (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.sku.trim() || !row.newSkuCode.trim())) || (labelingMode.value !== 'sku' && !row.fbaCartonMark.trim()))) { ElMessage.warning('请按当前换标类型填写完整明细'); return }
|
||
}
|
||
if (props.service !== 'transfer' || action === 'submit') {
|
||
try { await ElMessageBox.confirm(`确认提交${config.value.title}吗?`, '提交确认', { confirmButtonText: '提交', cancelButtonText: '取消' }) } catch { return }
|
||
}
|
||
saving.value = true
|
||
try {
|
||
const idempotencyKey = createIdempotencyKey()
|
||
if (props.service === 'labeling') {
|
||
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'
|
||
? cartonDetailRows.value.map((item) => ({ sourceInventoryBalanceId: item.sourceInventoryBalanceId, sourceInventoryPositionId: item.sourceInventoryPositionId, sourceInboundOrderId: item.sourceInboundOrderId, returnCartonId: item.returnCartonId, 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: form.destination, operationType: 'CARTON_LABEL' as const, skuItems: [], remark: item.remark || undefined }))
|
||
: buildSkuRelabelItems()
|
||
const selectedOperationType = labelingMode.value === 'carton' ? 'CARTON_LABEL' : labelingMode.value === 'sku' ? 'SKU_LABEL' : 'CARTON_AND_SKU'
|
||
if (items.some(item => item.operationType !== selectedOperationType)) throw new Error('一张申请只能选择一种换标类型,请核对绑定明细')
|
||
const created = await createRelabel({ customerOrderNo: sourceInboundOrders || '贴标申请', operationWarehouseId: form.warehouse, labelDeadline: form.labelDeadline, contactName: form.contact, contactPhone: form.phone, remark: form.remark || undefined, idempotencyKey, items,
|
||
deliveryMethod: labelingMode.value === 'sku' ? undefined : form.deliveryMethod as 'TRUCK' | 'EXPRESS' | 'AMAZON_PICKUP' | 'CUSTOMER_PICKUP',
|
||
printShippingLabel: labelingMode.value !== 'sku' && form.deliveryMethod === 'EXPRESS' && form.relabel })
|
||
try {
|
||
if (cartonTotalFile.value) await uploadRelabelFile(created.application.relabelNo, 'CARTON_TOTAL', cartonTotalFile.value)
|
||
if (skuTotalFile.value) await uploadRelabelFile(created.application.relabelNo, 'SKU_TOTAL', skuTotalFile.value)
|
||
if (shippingLabelFile.value) await uploadRelabelShippingLabel(created.application.relabelNo, shippingLabelFile.value)
|
||
} catch (error) {
|
||
// Creation already committed: return to the list even when an attachment still needs retrying.
|
||
await goToServiceList()
|
||
await ElMessageBox.alert(`申请 ${created.application.relabelNo} 已创建,部分附件上传失败:${error instanceof Error ? error.message : '上传失败'}。请在原单详情补传失败附件,不要重复下单。`, '申请已创建,附件待补传', { confirmButtonText: '知道了' })
|
||
return
|
||
}
|
||
ElMessage.success(`${config.value.title}提交成功`)
|
||
await goToServiceList()
|
||
return
|
||
} else if (props.service === 'disposal') {
|
||
const items = cargoRows.value.filter((item) => item.quantity > 0).map((item) => {
|
||
const belongs = discardSelectionSources.value.some(source => item.sourceType === 'INVENTORY'
|
||
? !!item.sourceInventoryPositionId && source.inventoryPositionId === item.sourceInventoryPositionId
|
||
: item.sourceType === 'RETURN_CARTON' ? !!item.returnCartonId && source.returnCartonId === item.returnCartonId
|
||
: !!item.sourceOrderId && source.orderId === item.sourceOrderId)
|
||
if (!belongs || item.quantity > Number(item.availableQuantity || 0)) {
|
||
throw new Error(`SKU ${item.sku} 的可废弃库存不足`)
|
||
}
|
||
return { orderId: item.sourceType === 'DROPSHIP' ? item.sourceOrderId : undefined,
|
||
inventoryPositionId: item.sourceType === 'INVENTORY' ? item.sourceInventoryPositionId : undefined,
|
||
returnCartonId: item.sourceType === 'RETURN_CARTON' ? item.returnCartonId : undefined,
|
||
skuCode: item.sku, discardPackages: item.quantity, cartonIds: item.discardCartonIds }
|
||
})
|
||
await createDiscard({ items, idempotencyKey, requestedPackages: discardQuantityMode.value ? discardRequestedPackages.value : undefined, remark: [form.reason, form.remark].filter(Boolean).join(';') || undefined })
|
||
} else {
|
||
const packageCount = cargoRows.value.reduce((sum, item) => sum + Number(item.expectedPackageCount || 0), 0)
|
||
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'
|
||
const selectedFba = fbaWarehouses.value.find((warehouse) => warehouse.warehouseCode === form.destination)
|
||
const payload: TransferCreate = { operatingWarehouseId: primaryWarehouse, destinationType: transferDestinationType.value, fbaWarehouseCode: transferDestinationType.value === 'FBA' ? form.destination || undefined : undefined, destinationWarehouseId: transferDestinationType.value === 'FBA' ? String(selectedFba?.id || '') || undefined : undefined, destinationWarehouseCode: transferDestinationType.value === 'FBA' ? form.destination || undefined : undefined, destinationWarehouseName: transferDestinationType.value === 'FBA' ? selectedFba?.warehouseName : undefined, privateDestinationAddress: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationAddress : undefined, privateDestinationPostalCode: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationPostalCode : undefined, privateDestinationContactName: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationContact : undefined, privateDestinationContactPhone: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationPhone : undefined, deliveryAddress: inventoryTransfer.value ? undefined : form.address, packageCount, volumeCbm: 0, deliveryMethod: inventoryTransfer.value ? undefined : form.deliveryMethod, estimatedArrivalAt: inventoryTransfer.value ? undefined : form.estimatedArrivalAt, estimatedShipAt: form.estimatedShipAt || undefined, fbaShipmentNo: transferDestinationType.value === 'FBA' ? form.fbaShipmentNo || undefined : undefined, trackingNo: !inventoryTransfer.value && form.deliveryMethod === 'CUSTOMER_DELIVERY' ? form.trackingNo || undefined : undefined, fbaBoxNos: customerCartonMarks.length ? customerCartonMarks.join(',') : undefined, remark: transferRemark || undefined, needRelabel: props.service === 'relabel' || form.relabel, labelDeadline: form.relabel ? form.labelDeadline : undefined, pickupContactName: !inventoryTransfer.value && form.deliveryMethod === 'WAREHOUSE_PICKUP' ? form.pickupContact : undefined, pickupContactPhone: !inventoryTransfer.value && form.deliveryMethod === 'WAREHOUSE_PICKUP' ? form.pickupPhone : undefined, relabelContactName: form.relabel ? form.relabelContact : undefined, relabelContactPhone: form.relabel ? form.relabelPhone : undefined, relabelDeliveryMethod: form.relabel && labelingMode.value !== 'sku' ? form.relabelDeliveryMethod as 'TRUCK' | 'EXPRESS' | 'AMAZON_PICKUP' | 'CUSTOMER_PICKUP' : undefined, idempotencyKey, items: cargoRows.value.map((item) => ({ skuCode: item.sku, productNameZh: item.name, quantity: item.quantity, packageCount: Number(item.expectedPackageCount), shippingMark: transferCustomerMarks(item).join(',') || undefined, cartonMarks: item.generatedCartonMarks?.slice(0, Number(item.expectedPackageCount)) })), relabelItems: form.relabel ? transferRelabelRows.value.map((item) => ({ operationType, customerCartonMarks: transferCustomerMarks(item), originalSku: item.sku, newSku: item.newSkuCode || undefined, quantity: item.quantity, fbaCartonMark: item.fbaCartonMark || undefined, destinationWarehouseCode: transferRelabelDefaultDestination.value || undefined, remark: item.remark || undefined })) : undefined }
|
||
if (props.service === 'transfer' && selectedTransferInventory.value) {
|
||
;(payload as TransferCreate).inboundCode = selectedTransferInventory.value
|
||
;(payload as TransferCreate).sourceLastlegOrderIds = selectedTransferSourceOrderIds.value
|
||
}
|
||
const created = editingTransferNo.value
|
||
? await updateTransfer(editingTransferNo.value, payload)
|
||
: await createTransfer(payload)
|
||
const transferNo = String(created.transferNo || '')
|
||
if (!transferNo) throw new Error('转运草稿已保存,但未返回转运单号')
|
||
if (action === 'submit') {
|
||
const submitted = await submitTransfer(transferNo)
|
||
if (form.relabel) {
|
||
const relabelNo = String(submitted.relabelNo || created.relabelNo || '')
|
||
if (!relabelNo) {
|
||
ElMessage.success('转运单已提交审核;PDA 入库确认后将自动生成关联换标单')
|
||
await ElMessageBox.alert(`转运单 ${transferNo} 已提交。关联换标单会在 PDA 完成入库、客户确认实测库存后自动生成;届时请进入关联换标单详情上传贴标总文件。`, '转运单已提交', { confirmButtonText: '知道了' })
|
||
await goToServiceList()
|
||
return
|
||
}
|
||
try {
|
||
if (cartonTotalFile.value) await uploadRelabelFile(relabelNo, 'CARTON_TOTAL', cartonTotalFile.value)
|
||
if (skuTotalFile.value) await uploadRelabelFile(relabelNo, 'SKU_TOTAL', skuTotalFile.value)
|
||
} catch (error) {
|
||
await ElMessageBox.alert(`转运单 ${transferNo} 已提交,但换标总文件上传失败:${error instanceof Error ? error.message : '上传失败'}。请进入关联换标单详情补传,不要重复提交转运单。`, '转运单已提交,附件待补传', { confirmButtonText: '知道了' })
|
||
await goToServiceList()
|
||
return
|
||
}
|
||
}
|
||
ElMessage.success('转运单已提交审核')
|
||
await goToServiceList()
|
||
return
|
||
}
|
||
}
|
||
ElMessage.success(props.service === 'transfer' ? '转运单草稿保存成功' : `${config.value.title}提交成功`)
|
||
await goToServiceList()
|
||
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '提交失败') }
|
||
finally { saving.value = false }
|
||
}
|
||
async function openDetail(value: Record<string, string>) {
|
||
if (['labeling', 'transfer', 'disposal'].includes(props.service) && !props.detailPage) {
|
||
const routeName = ({ labeling: 'overseas-labeling-detail', transfer: 'overseas-storage-transfer-detail', disposal: 'overseas-cargo-disposal-detail' } as Partial<Record<ServiceType, string>>)[props.service]
|
||
if (routeName) void router.push({ name: routeName, params: { detailNo: value['0'] } })
|
||
return
|
||
}
|
||
detailError.value = ''
|
||
if (props.service === 'labeling') selectedRelabelQuantity.value = null
|
||
try {
|
||
const detail = props.service === 'labeling' ? await getRelabel(value['0']) : props.service === 'disposal' ? await getDiscard(value['0']) : await getTransfer(value['0'])
|
||
const discardSummary = props.service === 'disposal' ? (detail as { summary?: ServiceDetail }).summary : undefined
|
||
showTransferCartons.value = false
|
||
selectedDetail.value = { ...value, ...(detail as ServiceDetail), ...(discardSummary || {}) }
|
||
selectedRelabelDetail.value = props.service === 'labeling' ? detail as RelabelDetail : null
|
||
if (props.service === 'labeling') {
|
||
const quantities = await getRelabelQuantities({ relabelNo: value['0'] }).catch(() => null)
|
||
if (selectedRelabelDetail.value?.application.relabelNo === value['0']) selectedRelabelQuantity.value = quantities?.orders[value['0']] || null
|
||
}
|
||
mode.value = 'detail'
|
||
if (props.service === 'labeling') void loadCompletionPhotos()
|
||
if (props.service === 'transfer') void loadMeasurementPhotos()
|
||
if (props.service === 'disposal') void loadDiscardPhotos()
|
||
} catch (error) { selectedDetail.value = null; selectedRelabelDetail.value = null; detailError.value = error instanceof Error ? error.message : '详情加载失败'; mode.value = 'detail'; ElMessage.error(detailError.value) }
|
||
}
|
||
async function loadDetailPage() {
|
||
await openDetail({ '0': detailNumber.value })
|
||
await loadWarehouses()
|
||
}
|
||
async function confirmTransferMeasurements() {
|
||
const transferNo = String(selectedDetail.value?.transferNo || selectedDetail.value?.['0'] || '')
|
||
if (!transferNo) return
|
||
openCompletionConfirmation(transferNo, 'transfer')
|
||
}
|
||
async function printTransferCartons(transferNo: string) {
|
||
if (props.service !== 'transfer') return
|
||
const printWindow = window.open('', '_blank')
|
||
if (!printWindow) { ElMessage.warning('浏览器阻止了打印窗口,请允许弹窗后重试'); return }
|
||
printWindow.opener = null
|
||
try {
|
||
if (!transferNo) { printWindow.close(); return }
|
||
const label = await getTransferCartonLabel(transferNo)
|
||
if (!label.cartons.length) { printWindow.close(); ElMessage.warning('暂无可打印的箱唛'); return }
|
||
printWindow.document.open(); printWindow.document.write(label.printableHtml); printWindow.document.close(); printWindow.focus(); printWindow.print()
|
||
} catch (error) { printWindow.close(); ElMessage.error(error instanceof Error ? error.message : '箱唛加载失败') }
|
||
}
|
||
function clearCompletionPhotos() {
|
||
for (const url of Object.values(completionPhotoUrls)) URL.revokeObjectURL(url)
|
||
for (const key of Object.keys(completionPhotoUrls)) delete completionPhotoUrls[key]
|
||
for (const key of Object.keys(completionPhotoLoading)) delete completionPhotoLoading[key]
|
||
for (const key of Object.keys(completionPhotoErrors)) delete completionPhotoErrors[key]
|
||
}
|
||
async function confirmTransferFromList(transferNo: string) {
|
||
openCompletionConfirmation(transferNo, 'transfer')
|
||
}
|
||
async function loadCompletionPhoto(fileId: string) {
|
||
const relabelNo = selectedRelabelDetail.value?.application.relabelNo
|
||
if (!relabelNo || completionPhotoLoading[fileId]) return
|
||
completionPhotoLoading[fileId] = true
|
||
completionPhotoErrors[fileId] = false
|
||
try {
|
||
const blob = await downloadRelabelCompletionPhoto(relabelNo, fileId)
|
||
if (selectedRelabelDetail.value?.application.relabelNo !== relabelNo) return
|
||
if (completionPhotoUrls[fileId]) URL.revokeObjectURL(completionPhotoUrls[fileId])
|
||
const url = URL.createObjectURL(blob)
|
||
completionPhotoUrls[fileId] = url
|
||
} catch { if (selectedRelabelDetail.value?.application.relabelNo === relabelNo) completionPhotoErrors[fileId] = true }
|
||
finally { completionPhotoLoading[fileId] = false }
|
||
}
|
||
async function loadCompletionPhotos() {
|
||
clearCompletionPhotos()
|
||
await Promise.all((selectedRelabelDetail.value?.completionEvidence?.photoFileIds || []).map(loadCompletionPhoto))
|
||
}
|
||
function clearMeasurementPhotos() {
|
||
for (const url of Object.values(measurementPhotoUrls)) URL.revokeObjectURL(url)
|
||
for (const key of Object.keys(measurementPhotoUrls)) delete measurementPhotoUrls[key]
|
||
for (const key of Object.keys(measurementPhotoLoading)) delete measurementPhotoLoading[key]
|
||
for (const key of Object.keys(measurementPhotoErrors)) delete measurementPhotoErrors[key]
|
||
}
|
||
async function loadMeasurementPhoto(fileId: string | number) {
|
||
const transferNo = selectedDetail.value?.transferNo
|
||
if (!transferNo || measurementPhotoLoading[fileId]) return
|
||
measurementPhotoLoading[fileId] = true
|
||
measurementPhotoErrors[fileId] = false
|
||
try {
|
||
const blob = await downloadTransferMeasurementPhoto(transferNo, fileId)
|
||
if (mode.value !== 'detail' || props.service !== 'transfer' || selectedDetail.value?.transferNo !== transferNo) return
|
||
if (measurementPhotoUrls[fileId]) URL.revokeObjectURL(measurementPhotoUrls[fileId])
|
||
measurementPhotoUrls[fileId] = URL.createObjectURL(blob)
|
||
} catch { if (mode.value === 'detail' && props.service === 'transfer' && selectedDetail.value?.transferNo === transferNo) measurementPhotoErrors[fileId] = true }
|
||
finally { measurementPhotoLoading[fileId] = false }
|
||
}
|
||
async function loadMeasurementPhotos() {
|
||
clearMeasurementPhotos()
|
||
await Promise.all((selectedDetail.value?.measurementPhotos || []).map((photo) => loadMeasurementPhoto(photo.fileId)))
|
||
}
|
||
async function confirmRelabelFromList(relabelNo: string) {
|
||
openCompletionConfirmation(relabelNo, 'labeling')
|
||
}
|
||
async function cancelSelectedDiscard() {
|
||
const no = String(selectedDetail.value?.discardNo || selectedDetail.value?.['0'] || '')
|
||
if (!no) return
|
||
try { await ElMessageBox.confirm('确认取消该废弃申请吗?', '取消确认', { confirmButtonText: '取消申请', cancelButtonText: '返回' }); await cancelDiscard(no); ElMessage.success('废弃申请已取消'); mode.value = 'list'; await load() }
|
||
catch (error) { if (error !== 'cancel') ElMessage.error(error instanceof Error ? error.message : '取消失败') }
|
||
}
|
||
function chooseRelabelFile(scope: 'CARTON_TOTAL' | 'SKU_TOTAL' | 'ITEM', itemId?: string) {
|
||
relabelFileScope.value = scope; relabelFileItemId.value = itemId; relabelFileInput.value?.click()
|
||
}
|
||
async function uploadSelectedRelabelFile(event: Event) {
|
||
const input = event.target as HTMLInputElement; const file = input.files?.[0]; input.value = ''
|
||
const relabelNo = selectedRelabelDetail.value?.application.relabelNo
|
||
if (!file || !relabelNo) return
|
||
try {
|
||
selectedRelabelDetail.value = await uploadRelabelFile(relabelNo, relabelFileScope.value, file, relabelFileItemId.value)
|
||
ElMessage.success('文件上传成功')
|
||
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '文件上传失败') }
|
||
}
|
||
async function loadWarehouses() {
|
||
fbaWarehousesLoading.value = true
|
||
fbaWarehousesError.value = ''
|
||
operationWarehousesError.value = ''
|
||
try {
|
||
const [fbaResult, operationResult] = await Promise.allSettled([
|
||
loadAllWarehouseRecords('FBA', getMerchantWarehouses),
|
||
loadAllWarehouseRecords('OVERSEAS', getMerchantWarehouses),
|
||
])
|
||
fbaWarehouses.value = fbaResult.status === 'fulfilled' ? fbaResult.value.filter((item) => item.status === 'ACTIVE' && item.warehouseType === 'FBA') : []
|
||
operationWarehouses.value = operationResult.status === 'fulfilled'
|
||
? operationResult.value.filter((item) => item.status === 'ACTIVE' && ['OVERSEAS', 'OVERSEAS_PARTNER'].includes(item.warehouseType))
|
||
: []
|
||
if (fbaResult.status === 'rejected') fbaWarehousesError.value = 'FBA 目的仓候选加载失败,请检查仓库主数据或权限'
|
||
if (operationResult.status === 'rejected') operationWarehousesError.value = '海外仓候选加载失败,请检查仓库主数据或数据库迁移'
|
||
} 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}` }
|
||
async function loadTransferInventoryCandidates(warehouseId: string) {
|
||
transferInventoryCandidates.value = []
|
||
transferAvailableSourceCounts.value = {}
|
||
if (!warehouseId) return
|
||
relabelCandidatesLoading.value = true
|
||
try {
|
||
// 共用接口包含退货历史及占用中的箱子,转运只展示当前可整箱使用的来源。
|
||
transferInventoryCandidates.value = (await listRelabelInventoryCandidates(warehouseId)).filter(isUnlabelledTransferCandidate).filter(item =>
|
||
item.sourceType === 'RETURN_CARTON' || item.sourceType === 'RETURN_PARCEL'
|
||
? Number(item.cartonAvailablePackages || 0) > 0
|
||
: Number(item.availablePackages || 0) > 0)
|
||
const sourceValues = [...new Set(transferInventoryCandidates.value.map(item => {
|
||
const returnNo = String(item.inboundOrderNo || '').trim()
|
||
return item.sourceType === 'RETURN_CARTON' || item.sourceType === 'RETURN_PARCEL'
|
||
? (returnNo ? `RETURN_ORDER:${returnNo}` : '') : String(item.inboundCode || '')
|
||
}).filter(Boolean))]
|
||
const results = await Promise.all(sourceValues.map(async (value) => {
|
||
try {
|
||
const cartons = await getTransferSourceCartons(warehouseId, value)
|
||
return [value, cartons.length] as const
|
||
} catch { return [value, 0] as const }
|
||
}))
|
||
transferAvailableSourceCounts.value = Object.fromEntries(results)
|
||
} catch (error) {
|
||
ElMessage.error(error instanceof Error ? error.message : '当前海外仓库存来源加载失败')
|
||
} finally { relabelCandidatesLoading.value = false }
|
||
}
|
||
async function selectTransferWarehouse() {
|
||
selectedOperationWarehouse.value = null
|
||
form.pickupContact = ''
|
||
form.pickupPhone = ''
|
||
form.relabelContact = ''
|
||
form.relabelPhone = ''
|
||
if (form.deliveryMethod === 'CUSTOMER_DELIVERY') form.address = ''
|
||
if (!form.warehouse) return
|
||
operationWarehouseDetailLoading.value = true
|
||
try {
|
||
const detail = await getMerchantWarehouseDetail(form.warehouse)
|
||
selectedOperationWarehouse.value = detail
|
||
form.relabelContact = detail.warehouse.contactName || ''
|
||
form.relabelPhone = detail.warehouse.contactPhone || ''
|
||
if (form.deliveryMethod === 'CUSTOMER_DELIVERY') form.address = operationWarehouseAddress.value
|
||
await loadTransferInventoryCandidates(form.warehouse)
|
||
} catch (error) {
|
||
ElMessage.error(error instanceof Error ? error.message : '中转仓联系信息加载失败')
|
||
} finally { operationWarehouseDetailLoading.value = false }
|
||
}
|
||
async function confirmSelectedRelabel() {
|
||
const relabelNo = selectedRelabelDetail.value?.application.relabelNo
|
||
if (relabelNo) openCompletionConfirmation(relabelNo, 'labeling')
|
||
}
|
||
async function resubmitRejectedRelabel() {
|
||
const application = selectedRelabelDetail.value?.application
|
||
if (!application || application.status !== 'REJECTED') return
|
||
try {
|
||
const { value } = await ElMessageBox.prompt('可先在下方替换贴标文件,再补充本次修改说明。', '修改后重新提交审核', {
|
||
inputType: 'textarea', inputValue: selectedRelabelDetail.value?.remark || '',
|
||
inputPlaceholder: '修改说明(选填)', confirmButtonText: '重新提交', cancelButtonText: '取消',
|
||
})
|
||
selectedRelabelDetail.value = await resubmitRelabel(application.relabelNo, {
|
||
labelDeadline: application.labelDeadline || '', contactName: application.contactName || '',
|
||
contactPhone: application.contactPhone || '', remark: value?.trim() || undefined,
|
||
idempotencyKey: createIdempotencyKey(),
|
||
})
|
||
ElMessage.success('贴标单已重新提交审核')
|
||
await goToServiceList()
|
||
} catch (error) {
|
||
if (error !== 'cancel' && error !== 'close') ElMessage.error(error instanceof Error ? error.message : '重新提交失败')
|
||
}
|
||
}
|
||
const transferInventoryOptions = computed(() => {
|
||
const grouped = new Map<string, { value: string; label: string; packages: number }>()
|
||
for (const item of transferInventoryCandidates.value) {
|
||
// 退货库存没有普通入仓码,使用带前缀的退货来源编码,避免提交时被当作普通入仓单查询。
|
||
const returnNo = String(item.inboundOrderNo || '').trim()
|
||
const value = item.sourceType === 'RETURN_CARTON' || item.sourceType === 'RETURN_PARCEL'
|
||
? (returnNo ? `RETURN_ORDER:${returnNo}` : '')
|
||
: String(item.inboundCode || '')
|
||
if (!value) continue
|
||
const packages = item.sourceType === 'RETURN_CARTON' || item.sourceType === 'RETURN_PARCEL'
|
||
? inventoryQuantity(item.cartonAvailablePackages || 0)
|
||
: inventoryQuantity(item.availablePackages)
|
||
const current = grouped.get(value)
|
||
if (current) current.packages += packages
|
||
else if ((transferAvailableSourceCounts.value[value] ?? 1) > 0) grouped.set(value, { value, label: value.startsWith('RETURN_ORDER:') ? `退货单|${returnNo}` : `${item.inboundOrderNo || value}|${value}`, packages })
|
||
}
|
||
return [...grouped.values()]
|
||
})
|
||
/** 已完成或部分完成换标的退货实物箱不再作为原箱来源;普通来源由逐箱接口过滤。 */
|
||
function isUnlabelledTransferCandidate(item: RelabelInventoryCandidate) {
|
||
if (item.sourceType !== 'RETURN_CARTON' && item.sourceType !== 'RETURN_PARCEL') return true
|
||
return !['COMPLETED', 'PARTIALLY_COMPLETED'].includes(item.relabelStatus || '')
|
||
&& inventoryQuantity(item.relabelledQuantity || item.relabelledPackages) === 0
|
||
}
|
||
async function selectTransferInventory(value: string) {
|
||
selectedTransferInventory.value = value || ''
|
||
selectedTransferSourceOrderIds.value = []
|
||
transferSourceCartons.value = []; transferSourceError.value = ''; ++transferSourceRequest
|
||
if (!value) { form.inboundNo = ''; cargoRows.value = [createCargoRow()]; transferSourceLoading.value = false; return }
|
||
if (transferDestinationType.value === 'NONE') transferDestinationType.value = 'FBA'
|
||
void loadTransferSourceCartons()
|
||
form.inboundNo = value
|
||
// The inventory-candidates response already contains the merchant-visible
|
||
// SKU/name/package data. Do not call the platform-only last-leg orders API
|
||
// here: merchants are not authorized for that endpoint (A0403), and the
|
||
// transfer service can derive source orders from inboundCode when IDs are
|
||
// omitted. Aggregate candidates for the selected inbound into cargo rows.
|
||
const isReturnSource = value.startsWith('RETURN_ORDER:')
|
||
const returnNo = isReturnSource ? value.slice('RETURN_ORDER:'.length) : ''
|
||
const candidates = transferInventoryCandidates.value.filter((item) => isReturnSource
|
||
? (item.sourceType === 'RETURN_CARTON' || item.sourceType === 'RETURN_PARCEL') && String(item.inboundOrderNo || '') === returnNo
|
||
: String(item.inboundCode || '') === value)
|
||
const grouped = new Map<string, CargoRow>()
|
||
for (const candidate of candidates) {
|
||
const candidateRows = isReturnSource
|
||
? (candidate.cartonSkuItems?.length ? candidate.cartonSkuItems : candidate.skuItems || []).map(item => ({ sku: item.skuCode, name: candidate.productNameZh || '', quantity: inventoryQuantity(item.quantity), packageCount: inventoryQuantity(candidate.cartonAvailablePackages || 0) }))
|
||
: [{ sku: String(candidate.skuCode || ''), name: candidate.productNameZh || '', quantity: inventoryQuantity(candidate.availablePackages), packageCount: inventoryQuantity(candidate.availablePackages) }]
|
||
for (const row of candidateRows) {
|
||
if (!row.sku) continue
|
||
const current = grouped.get(row.sku)
|
||
if (current) { current.quantity += row.quantity; current.expectedPackageCount = Number(current.expectedPackageCount || 0) + row.packageCount; current.availableQuantity = Number(current.availableQuantity || 0) + row.quantity }
|
||
else grouped.set(row.sku, { ...createCargoRow(), sku: row.sku, name: row.name, quantity: row.quantity, expectedPackageCount: row.packageCount, availableQuantity: row.quantity })
|
||
}
|
||
}
|
||
cargoRows.value = [...grouped.values()]
|
||
try {
|
||
if (isReturnSource) return
|
||
const historical = await listRelabelHistoricalOrders(form.warehouse, value)
|
||
const sourceIds = historical.map((item) => String(item.id || '')).filter(Boolean)
|
||
if (selectedTransferInventory.value === value) selectedTransferSourceOrderIds.value = sourceIds
|
||
} catch (error) {
|
||
ElMessage.error(error instanceof Error ? error.message : '库存来源订单加载失败')
|
||
}
|
||
}
|
||
function selectTransferInboundMethod(value: string | number | boolean | undefined) {
|
||
form.address = value === 'CUSTOMER_DELIVERY' ? operationWarehouseAddress.value : ''
|
||
}
|
||
function selectTransferDestinationType(value: string | number | boolean | undefined) {
|
||
if (value !== 'FBA') { form.destination = ''; form.fbaShipmentNo = '' }
|
||
if (value !== 'PRIVATE') { form.privateDestinationAddress = ''; form.privateDestinationPostalCode = ''; form.privateDestinationContact = ''; form.privateDestinationPhone = '' }
|
||
}
|
||
async function selectOperationWarehouse() {
|
||
cargoRows.value = []
|
||
transferRelabelRows.value = []
|
||
skuRelabelRows.value = []
|
||
importedRelabelBindings.value = []
|
||
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) {
|
||
const rows: CargoRow[] = []
|
||
const selectedCandidates = currentRelabelCandidates.value.filter((item) => {
|
||
return item.returnCartonId ? returnSourceNos(item).includes(inboundOrderId.slice('RETURN_ORDER:'.length)) : String(item.inboundOrderId) === inboundOrderId
|
||
})
|
||
if (inboundOrderId.startsWith('RETURN_ORDER:')) {
|
||
for (const candidate of selectedCandidates) rows.push({
|
||
id: ++cargoSequence, sku: candidate.skuItems?.[0]?.skuCode || candidate.skuCode || '',
|
||
name: candidate.productNameZh || '', included: false, quantity: 1, availableQuantity: 1,
|
||
returnCartonId: String(candidate.returnCartonId), sourceInboundOrderNo: candidate.inboundOrderNo,
|
||
fnsku: '', shipmentNo: candidate.inboundOrderNo || '', newSkuCode: '', shippingMark: '', customerCartonMarks: '',
|
||
cartonMark: candidate.cartonMark || '', fbaCartonMark: '', destinationWarehouseId: '', remark: '',
|
||
})
|
||
} else for (const candidate of selectedCandidates) {
|
||
for (const position of candidate.positions) rows.push(...Array.from({ length: Math.min(inventoryQuantity(position.availablePackages), 500) }, () => ({
|
||
id: ++cargoSequence, sku: candidate.skuCode || '', name: candidate.productNameZh || '库存货物',
|
||
included: false, quantity: 1, availableQuantity: 1,
|
||
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,
|
||
fnsku: '', shipmentNo: '', newSkuCode: '', shippingMark: '', customerCartonMarks: '', cartonMark: '', fbaCartonMark: '',
|
||
destinationWarehouseId: candidate.destinationFbaWarehouseCode || '', remark: '',
|
||
})))
|
||
}
|
||
cargoRows.value = rows.slice(0, 500)
|
||
if (cargoRows.value.length && !inboundOrderId.startsWith('RETURN_ORDER:')) cargoRows.value[0].included = true
|
||
skuRelabelRows.value = []
|
||
importedRelabelBindings.value = []
|
||
shippingLabelFile.value = null
|
||
cartonTotalFile.value = null
|
||
skuTotalFile.value = null
|
||
if (!isReturnRelabelSource.value && labelingMode.value !== 'carton') addSkuRelabelRow()
|
||
}
|
||
function addReturnSkuBinding(source: RelabelInventoryCandidate, skuCode: string) {
|
||
const sku = source.skuItems?.find(item => item.skuCode === skuCode)
|
||
if (!sku) return
|
||
const row = skuRelabelRows.value.find(item => item.cartonMark === source.cartonMark && item.originalSku === skuCode)
|
||
if (row) return
|
||
const sourceRow = cargoRows.value.find(item => item.returnCartonId === String(source.returnCartonId))
|
||
skuRelabelRows.value.push({ id: ++skuRelabelSequence, sourceRowId: sourceRow?.id || null, cartonMark: source.cartonMark || '', originalSku: skuCode, newSku: '', quantity: Math.max(1, Number(sku.quantity)), fbaCartonMark: '', shipmentNo: '', destinationWarehouseId: '', remark: '' })
|
||
}
|
||
function addReturnBoxBinding(source: RelabelInventoryCandidate) {
|
||
if (labelingMode.value !== 'carton') {
|
||
for (const sku of source.skuItems || []) addReturnSkuBinding(source, sku.skuCode)
|
||
return
|
||
}
|
||
if (cartonDetailRows.value.some(item => item.cartonMark === source.cartonMark)) return
|
||
cargoRows.value.push({ id: ++cargoSequence, included: true, sku: source.skuItems?.map(item => item.skuCode).join(',') || '', name: source.productNameZh || '', shipmentNo: '', cartonMark: source.cartonMark || '', fbaCartonMark: '', remark: '', sourceInventoryBalanceId: undefined, sourceInventoryPositionId: undefined, sourceInboundOrderId: undefined, returnCartonId: String(source.returnCartonId), quantity: 1, expectedPackageCount: 1, fnsku: '', newSkuCode: '', shippingMark: '', customerCartonMarks: '', destinationWarehouseId: '' })
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<section class="warehouse-service-page" :class="{ 'warehouse-create client-ui': isServiceCreate, 'warehouse-labeling-create': isLabelingCreate, 'warehouse-transfer-create': isTransferCreate, 'warehouse-disposal-create': isDisposalCreate, 'warehouse-service-page--detail': mode === 'detail', 'warehouse-service-page--transfer': props.service === 'transfer', 'warehouse-service-page--disposal-form': props.service === 'disposal' && (mode === 'create' || mode === 'edit'), 'warehouse-service-page--return-relabel': isReturnRelabelSource }">
|
||
<template v-if="mode === 'list'">
|
||
<QueryToolbar v-model="listQueryModel" :class="{ 'labeling-query': props.service === 'labeling', 'transfer-query': props.service === 'transfer' || props.service === 'disposal', 'disposal-query': props.service === 'disposal' }" layout="grid" :ariaLabel="`${config.title}筛选`" :fields="listQueryFields" :search-on-reset="false" @search="query" @reset="resetListQuery" />
|
||
<div class="warehouse-card warehouse-table-card">
|
||
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="submittedRows" :columns="listColumns" :loading="loading" :total="total" :page-sizes="[15, 30, 50, 100]" :row-actions="listRowActions" :empty-text="config.emptyText">
|
||
<template #toolbar-actions><el-button v-if="canCreate" type="primary" @click="openCreate"><Plus :size="16" />{{ props.service === 'relabel' ? '新建退货换标单' : props.service === 'transfer' ? '新增订单' : `新增${config.title}` }}</el-button></template>
|
||
<template #transferRelabelStatus="{ row }"><el-tag :type="transferRelabelStatusTagType(row.__relabelStatus)" effect="light">{{ row.__relabelStatusLabel }}</el-tag></template>
|
||
<template #labelingQuantity="{ row, column }"><span :class="{ 'warehouse-missing-value': row[column.property] === '—' }">{{ row[column.property] }}</span></template>
|
||
<template #labelingType="{ row }"><span v-if="row['6'] === '—'">—</span><span v-else class="labeling-type-tags"><el-tag v-for="label in labelingTypeLabels(row['6'])" :key="label" :type="labelingTypeTagType(label)" size="small" effect="light">{{ label }}</el-tag></span></template>
|
||
<template #labelingStatus="{ row }"><el-tag :type="labelingStatusTagType(row.__status)" size="small" effect="light">{{ statusLabel(row.__status) }}</el-tag></template>
|
||
<template #transferStatus="{ row }"><el-tag :type="transferStatusTagType(row.__status)" effect="light">{{ row.__statusLabel }}</el-tag></template>
|
||
</DataTable>
|
||
</div>
|
||
</template>
|
||
|
||
<template v-else-if="mode === 'create' || mode === 'edit'">
|
||
<div class="warehouse-page-heading"><h2>{{ mode === 'edit' ? `编辑转运草稿 ${editingTransferNo}` : config.createTitle }}</h2><el-button @click="returnToList"><ArrowLeft :size="16" />返回列表</el-button></div>
|
||
<component :is="isServiceCreate ? ElForm : 'form'" ref="labelingFormRef" :model="isLabelingCreate ? labelingFormModel : isTransferCreate ? transferFormModel : isDisposalCreate ? disposalFormModel : undefined" :rules="isLabelingCreate ? labelingFormRules : isTransferCreate ? transferFormRules : isDisposalCreate ? disposalFormRules : undefined" :disabled="isServiceCreate ? saving : undefined" label-position="top" :validate-on-rule-change="false" :scroll-to-error="isServiceCreate" :class="{ 'warehouse-transfer-form': props.service === 'transfer', 'warehouse-transfer-form--relabel': props.service === 'transfer' && form.relabel }" :style="props.service === 'transfer' ? { '--transfer-left': `${transferPaneShare}fr`, '--transfer-right': `${100 - transferPaneShare}fr` } : undefined" @submit.prevent="submit('submit')">
|
||
<div id="transfer-main-pane" class="warehouse-card warehouse-form-section warehouse-transfer-main-card">
|
||
<div v-if="isTransferCreate || isDisposalCreate" class="warehouse-order-info-heading"><h3>{{ isTransferCreate ? '转运信息' : '基础信息' }}</h3></div>
|
||
<div v-if="props.service === 'labeling'" class="warehouse-order-info-heading">
|
||
<h3>订单信息</h3>
|
||
<el-button text type="primary" native-type="button" :aria-expanded="!orderInfoCollapsed" @click="orderInfoCollapsed = !orderInfoCollapsed">{{ orderInfoCollapsed ? '展开信息' : '收起信息' }}</el-button>
|
||
</div>
|
||
<h3 v-else-if="!isTransferCreate && !isDisposalCreate">{{ props.service === 'transfer' ? '转运信息' : props.service === 'relabel' ? '退货信息' : '基础信息' }}</h3>
|
||
<div v-if="props.service === 'labeling' && orderInfoCollapsed" class="warehouse-order-info-summary">
|
||
<span>{{ operationWarehouses.find(item => String(item.id) === form.warehouse)?.warehouseName || '未选择操作仓' }}</span>
|
||
<span>{{ relabelInboundOptions.find(item => item.value === selectedRelabelInboundOrderId)?.inboundOrderNo || '未选择来源单据' }}</span>
|
||
</div>
|
||
<div v-show="props.service !== 'labeling' || !orderInfoCollapsed" class="warehouse-form-grid">
|
||
<template v-if="isLabelingCreate">
|
||
<el-form-item label="海外操作仓(中转仓)" prop="warehouse">
|
||
<el-select v-model="form.warehouse" :loading="fbaWarehousesLoading || operationWarehouseDetailLoading" :disabled="Boolean(operationWarehousesError)" clearable placeholder="请选择海外操作仓" popper-class="labeling-create-popper" @change="selectOperationWarehouse"><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>
|
||
</el-form-item>
|
||
<el-form-item label="选择入仓单或退货单" prop="sourceOrder">
|
||
<el-select v-model="selectedRelabelInboundOrderId" filterable clearable :filter-method="(value: string) => relabelInventorySearch = value" :loading="relabelCandidatesLoading" :disabled="!form.warehouse" no-data-text="暂无匹配的可选库存" placeholder="请选择入仓单或退货单" popper-class="labeling-create-popper" @change="selectRelabelInventory"><el-option v-for="option in visibleRelabelInboundOptions.filter(item => !relabelInventorySearch.trim() || item.inboundOrderNo.toUpperCase().includes(relabelInventorySearch.trim().toUpperCase()) || item.inboundCode.toUpperCase().includes(relabelInventorySearch.trim().toUpperCase()))" :key="option.value" :label="relabelOptionLabel(option)" :value="option.value" :disabled="option.availablePackages <= 0" /></el-select>
|
||
</el-form-item>
|
||
<el-form-item v-if="labelingMode !== 'sku'" label="FBA 目的仓" prop="destination"><el-select v-model="form.destination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder="请选择 FBA 目的仓" popper-class="labeling-create-popper"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select></el-form-item>
|
||
<el-form-item label="海外仓签收联系人" prop="contact"><el-input v-model="form.contact" :placeholder="form.warehouse && !form.contact ? '仓库未维护,请填写签收联系人' : '请输入海外仓签收联系人'" /></el-form-item>
|
||
<el-form-item label="海外仓联系电话" prop="phone"><el-input v-model="form.phone" type="tel" :placeholder="form.warehouse && !form.phone ? '仓库未维护,请填写联系电话' : '请输入海外仓联系电话'" /></el-form-item>
|
||
<el-form-item label="贴标截止时间" prop="labelDeadline"><el-date-picker v-model="form.labelDeadline" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择贴标截止时间" popper-class="labeling-create-popper" /></el-form-item>
|
||
<el-form-item v-if="labelingMode !== 'sku'" label="换标后派送方式" prop="deliveryMethod"><el-select v-model="form.deliveryMethod" placeholder="请选择换标后派送方式" popper-class="labeling-create-popper"><el-option label="卡车派送" value="TRUCK" /><el-option label="快递派送" value="EXPRESS" /><el-option label="亚马逊自提" value="AMAZON_PICKUP" /><el-option label="客户自提" value="CUSTOMER_PICKUP" /></el-select></el-form-item>
|
||
<el-form-item v-if="labelingMode !== 'sku' && form.deliveryMethod === 'EXPRESS'" label="是否打印面单"><el-checkbox v-model="form.relabel">需要仓库打印面单</el-checkbox></el-form-item>
|
||
<el-form-item v-if="canManageRelabelFiles && labelingMode !== 'sku' && form.deliveryMethod === 'EXPRESS' && form.relabel" label="上传面单文件" prop="shippingLabelFile"><input class="warehouse-native-file" type="file" accept="application/pdf,image/*,.pdf" @change="selectShippingLabel" /></el-form-item>
|
||
<el-form-item label="贴标要求"><el-input v-model="form.requirement" placeholder="请输入贴标要求(选填)" /></el-form-item>
|
||
<el-form-item class="warehouse-span-2" label="备注"><el-input v-model="form.remark" type="textarea" :rows="4" resize="vertical" placeholder="请输入其他补充信息(选填)" /></el-form-item>
|
||
</template>
|
||
<template v-else-if="isTransferCreate">
|
||
<el-form-item label="入库海外仓" prop="warehouse"><el-select v-model="form.warehouse" :loading="fbaWarehousesLoading || operationWarehouseDetailLoading" :disabled="Boolean(operationWarehousesError)" clearable placeholder="请选择入库海外仓" popper-class="transfer-create-popper" @change="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></el-form-item>
|
||
<el-form-item label="库存转运来源(可选)"><el-select v-model="selectedTransferInventory" filterable clearable :loading="relabelCandidatesLoading" :disabled="!form.warehouse" no-data-text="当前海外仓暂无可用库存" placeholder="请选择库存来源(选填)" popper-class="transfer-create-popper" @change="selectTransferInventory"><el-option v-for="option in transferInventoryOptions" :key="option.value" :label="`${option.label}|可用 ${option.packages} 箱`" :value="option.value" /></el-select></el-form-item>
|
||
<el-form-item v-if="!inventoryTransfer" class="warehouse-transfer-choice" label="入仓方式" prop="deliveryMethod"><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></el-form-item>
|
||
<template v-if="!inventoryTransfer && form.deliveryMethod === 'WAREHOUSE_PICKUP'">
|
||
<el-form-item class="warehouse-span-2" label="提货地址" prop="address"><el-input v-model="form.address" placeholder="请输入详细提货地址" /></el-form-item>
|
||
<el-form-item label="提货联系人" prop="pickupContact"><el-input v-model="form.pickupContact" placeholder="请输入提货联系人" maxlength="128" /></el-form-item>
|
||
<el-form-item label="提货联系人电话" prop="pickupPhone"><el-input v-model="form.pickupPhone" type="tel" placeholder="请输入提货联系电话" maxlength="64" /></el-form-item>
|
||
</template>
|
||
<div v-if="!inventoryTransfer && form.deliveryMethod === 'CUSTOMER_DELIVERY'" v-loading="operationWarehouseDetailLoading" class="warehouse-receiving-card warehouse-span-2">
|
||
<div class="warehouse-span-2"><span>海外仓收货信息</span><strong>{{ operationWarehouseAddress || '该海外仓暂未维护收货地址' }}</strong></div>
|
||
<el-form-item class="warehouse-span-2" label="送货地址" prop="address"><el-input v-model="form.address" placeholder="请输入详细送货地址" /></el-form-item>
|
||
<div><span>收货联系人</span><strong>{{ selectedOperationWarehouse?.warehouse.contactName || '未维护' }}</strong></div><div><span>联系电话</span><strong>{{ selectedOperationWarehouse?.warehouse.contactPhone || '未维护' }}</strong></div>
|
||
</div>
|
||
<el-form-item v-if="!inventoryTransfer" label="预计入仓时间" prop="estimatedArrivalAt"><el-date-picker v-model="form.estimatedArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择预计入仓时间" popper-class="transfer-create-popper" /></el-form-item>
|
||
<el-form-item label="期望发货时间"><el-date-picker v-model="form.estimatedShipAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择期望发货时间(选填)" popper-class="transfer-create-popper" /></el-form-item>
|
||
<el-form-item class="warehouse-transfer-choice" label="目的去向"><el-radio-group v-model="transferDestinationType" @change="selectTransferDestinationType"><el-radio-button v-if="!inventoryTransfer" value="NONE">暂未确定</el-radio-button><el-radio-button value="FBA">FBA 仓</el-radio-button><el-radio-button value="PRIVATE">私人仓</el-radio-button></el-radio-group></el-form-item>
|
||
<template v-if="transferDestinationType === 'FBA'">
|
||
<el-form-item label="FBA 目的仓" prop="destination"><el-select v-model="form.destination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder="请选择 FBA 目的仓" popper-class="transfer-create-popper"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select></el-form-item>
|
||
<el-form-item label="FBA 货件编号"><el-input v-model="form.fbaShipmentNo" placeholder="请输入 FBA 货件编号(选填)" /></el-form-item>
|
||
</template>
|
||
<template v-if="transferDestinationType === 'PRIVATE'">
|
||
<el-form-item class="warehouse-span-2" label="私人仓地址" prop="privateDestinationAddress"><el-input v-model="form.privateDestinationAddress" maxlength="1000" placeholder="请输入私人仓详细地址" /></el-form-item>
|
||
<el-form-item label="邮编" prop="privateDestinationPostalCode"><el-input v-model="form.privateDestinationPostalCode" maxlength="32" placeholder="请输入邮编" /></el-form-item>
|
||
<el-form-item label="私人仓联系人" prop="privateDestinationContact"><el-input v-model="form.privateDestinationContact" maxlength="128" placeholder="请输入私人仓联系人" /></el-form-item>
|
||
<el-form-item label="联系电话" prop="privateDestinationPhone"><el-input v-model="form.privateDestinationPhone" type="tel" maxlength="64" placeholder="请输入联系电话" /></el-form-item>
|
||
</template>
|
||
<el-form-item v-if="!inventoryTransfer && form.deliveryMethod === 'CUSTOMER_DELIVERY'" label="物流跟踪号"><el-input v-model="form.trackingNo" placeholder="请输入物流跟踪号(选填)" /></el-form-item>
|
||
<el-form-item class="warehouse-transfer-choice" label="是否贴标" required><el-radio-group v-model="form.relabel"><el-radio-button v-if="!transferRelabelRequired" :value="false">不需要贴标</el-radio-button><el-radio-button :value="true">需要贴标</el-radio-button></el-radio-group></el-form-item>
|
||
<el-form-item class="warehouse-span-2" label="备注"><el-input v-model="form.remark" type="textarea" :rows="3" resize="vertical" placeholder="请输入转运要求或其他补充信息(选填)" /></el-form-item>
|
||
</template>
|
||
<template v-else-if="isDisposalCreate">
|
||
<el-form-item label="操作仓" prop="warehouse"><el-select v-model="form.warehouse" filterable clearable :loading="fbaWarehousesLoading" :disabled="Boolean(operationWarehousesError)" placeholder="请选择操作仓" popper-class="disposal-create-popper" @change="selectDiscardWarehouse"><el-option v-for="warehouse in operationWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="String(warehouse.id)" /></el-select><small v-if="operationWarehousesError" class="warehouse-hint">{{ operationWarehousesError }}</small></el-form-item>
|
||
<el-form-item label="选择入库单" prop="sourceOrder"><el-select v-model="selectedDiscardOrderId" filterable clearable :loading="discardCandidatesLoading" :disabled="!form.warehouse" no-data-text="当前海外仓暂无可废弃库存" placeholder="请选择入库单" popper-class="disposal-create-popper" @change="selectDiscardInbound"><el-option v-for="candidate in discardDocuments" :key="candidate.orderId" :label="`${candidate.sourceType === 'RETURN_CARTON' ? '退货|' : ''}${candidate.inboundOrderNo || candidate.inboundCode || '—'}|可用 ${candidate.availablePackages} 箱`" :value="candidate.orderId" /></el-select></el-form-item>
|
||
<el-form-item label="废弃原因" prop="reason"><el-select v-model="form.reason" placeholder="请选择废弃原因" popper-class="disposal-create-popper"><el-option label="残损不可售" value="残损不可售" /><el-option label="其他" value="其他" /></el-select></el-form-item>
|
||
<el-form-item class="warehouse-span-2" label="备注" prop="remark" :rules="discardQuantityMode ? [{ required: true, whitespace: true, message: '请填写废弃备注', trigger: 'blur' }] : []"><el-input v-model="form.remark" type="textarea" :rows="3" resize="vertical" :placeholder="discardQuantityMode ? '请填写废弃备注' : '请输入废弃要求或其他补充信息(选填)'" /></el-form-item>
|
||
</template>
|
||
<template v-else>
|
||
<label v-if="props.service === 'disposal'" class="warehouse-disposal-field"><span><i>*</i>操作仓</span><el-select v-model="form.warehouse" filterable clearable :loading="fbaWarehousesLoading" :disabled="Boolean(operationWarehousesError)" placeholder="" @change="selectDiscardWarehouse"><el-option v-for="warehouse in operationWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="String(warehouse.id)" /></el-select><small v-if="operationWarehousesError" class="warehouse-hint">{{ operationWarehousesError }}</small></label>
|
||
<label v-if="props.service === 'disposal'" class="warehouse-disposal-field"><span><i>*</i>选择入库单</span><el-select v-model="selectedDiscardOrderId" filterable clearable :loading="discardCandidatesLoading" :disabled="!form.warehouse" no-data-text="当前海外仓暂无可废弃库存" placeholder="" @change="selectDiscardInbound"><el-option v-for="candidate in discardDocuments" :key="candidate.orderId" :label="`${candidate.sourceType === 'RETURN_CARTON' ? '退货|' : ''}${candidate.inboundOrderNo || candidate.inboundCode || '—'}|可用 ${candidate.availablePackages} 箱`" :value="candidate.orderId" /></el-select></label>
|
||
<label v-if="props.service === 'relabel'"><span><i>*</i>进仓编号</span><el-input v-model="form.inboundNo" placeholder="" /></label>
|
||
<label v-if="props.service === 'relabel'"><span><i>*</i>来源 FBA 仓库</span><el-select v-model="form.sourceWarehouse" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" placeholder=""><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select></label>
|
||
<label v-else-if="props.service === 'disposal'" class="warehouse-disposal-field"><span><i>*</i>废弃原因</span><el-select v-model="form.reason" placeholder=""><el-option label="残损不可售" value="残损不可售" /><el-option label="其他" value="其他" /></el-select></label>
|
||
<label v-else><span><i>*</i>{{ props.service === 'labeling' ? '海外操作仓(中转仓)' : props.service === 'transfer' ? '入库海外仓' : '操作仓' }}</span><el-select v-model="form.warehouse" :loading="fbaWarehousesLoading || operationWarehouseDetailLoading" :disabled="Boolean(operationWarehousesError)" clearable placeholder="" @change="props.service === 'labeling' ? selectOperationWarehouse() : props.service === 'transfer' && selectTransferWarehouse()"><el-option v-for="warehouse in operationWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="String(warehouse.id)" /></el-select><small v-if="operationWarehousesError" class="warehouse-hint">{{ operationWarehousesError }}</small></label>
|
||
<label v-if="props.service === 'labeling'"><span><i>*</i>选择入仓单或退货单</span><el-select v-model="selectedRelabelInboundOrderId" filterable clearable :filter-method="(value: string) => relabelInventorySearch = value" :loading="relabelCandidatesLoading" :disabled="!form.warehouse" no-data-text="暂无匹配的可选库存" placeholder="" @change="selectRelabelInventory"><el-option v-for="option in visibleRelabelInboundOptions.filter(item => !relabelInventorySearch.trim() || item.inboundOrderNo.toUpperCase().includes(relabelInventorySearch.trim().toUpperCase()) || item.inboundCode.toUpperCase().includes(relabelInventorySearch.trim().toUpperCase()))" :key="option.value" :label="relabelOptionLabel(option)" :value="option.value" :disabled="option.availablePackages <= 0" /></el-select></label>
|
||
<label v-if="props.service === 'transfer'"><span>库存转运来源(可选)</span><el-select v-model="selectedTransferInventory" filterable clearable :loading="relabelCandidatesLoading" :disabled="!form.warehouse" no-data-text="当前海外仓暂无可用库存" placeholder="" @change="selectTransferInventory"><el-option v-for="option in transferInventoryOptions" :key="option.value" :label="`${option.label}|可用 ${option.packages} 箱`" :value="option.value" /></el-select></label>
|
||
<label v-if="props.service === 'labeling' && labelingMode !== 'sku'"><span><i>*</i>FBA 目的仓</span><el-select v-model="form.destination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder=""><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select></label>
|
||
<div v-if="props.service === 'transfer' && !inventoryTransfer" class="warehouse-choice-field warehouse-span-2"><span><i>*</i>入仓方式</span><el-radio-group v-model="form.deliveryMethod" @change="selectTransferInboundMethod"><el-radio-button value="WAREHOUSE_PICKUP">仓库上门提货</el-radio-button><el-radio-button value="CUSTOMER_DELIVERY">客户送货到仓</el-radio-button></el-radio-group></div>
|
||
<label v-if="props.service === 'transfer' && !inventoryTransfer && form.deliveryMethod === 'WAREHOUSE_PICKUP'" class="pickup-address-field"><span><i>*</i>提货地址</span><el-input v-model="form.address" placeholder="" /></label>
|
||
<label v-if="props.service === 'transfer' && !inventoryTransfer && form.deliveryMethod === 'WAREHOUSE_PICKUP'"><span><i>*</i>提货联系人</span><el-input v-model="form.pickupContact" placeholder="" maxlength="128" /></label>
|
||
<label v-if="props.service === 'transfer' && !inventoryTransfer && form.deliveryMethod === 'WAREHOUSE_PICKUP'"><span><i>*</i>提货联系人电话</span><el-input v-model="form.pickupPhone" placeholder="" maxlength="64" /></label>
|
||
<div v-if="props.service === 'transfer' && !inventoryTransfer && form.deliveryMethod === 'CUSTOMER_DELIVERY'" v-loading="operationWarehouseDetailLoading" class="warehouse-receiving-card warehouse-span-2">
|
||
<div><span>海外仓收货信息</span><strong>{{ operationWarehouseAddress || '该海外仓暂未维护收货地址' }}</strong></div>
|
||
<label class="warehouse-span-2"><span><i>*</i>送货地址</span><el-input v-model="form.address" placeholder="" /></label>
|
||
<div><span>收货联系人</span><strong>{{ selectedOperationWarehouse?.warehouse.contactName || '未维护' }}</strong></div>
|
||
<div><span>联系电话</span><strong>{{ selectedOperationWarehouse?.warehouse.contactPhone || '未维护' }}</strong></div>
|
||
</div>
|
||
<label v-if="props.service === 'relabel'"><span><i>*</i>目的 FBA 仓</span><el-select v-model="form.destination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" placeholder=""><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select></label>
|
||
<label v-if="props.service === 'relabel'"><span><i>*</i>派送方式</span><el-select v-model="form.deliveryMethod" placeholder=""><el-option label="卡车派送" value="卡车派送" /><el-option label="快递派送" value="快递派送" /></el-select></label>
|
||
<label v-if="props.service === 'relabel'"><span><i>*</i>退货原因</span><el-select v-model="form.reason" placeholder=""><el-option label="客户退货" value="客户退货" /><el-option label="亚马逊移除" value="亚马逊移除" /><el-option label="残损不可售" value="残损不可售" /></el-select></label>
|
||
<label v-if="props.service === 'labeling'"><span><i>*</i>海外仓签收联系人<em v-if="form.warehouse && !form.contact" class="warehouse-unmaintained">未维护</em></span><el-input v-model="form.contact" placeholder="" /></label>
|
||
<label v-if="props.service === 'labeling'"><span><i>*</i>海外仓联系电话<em v-if="form.warehouse && !form.phone" class="warehouse-unmaintained">未维护</em></span><el-input v-model="form.phone" placeholder="" /></label>
|
||
<label v-if="props.service === 'labeling'"><span><i>*</i>贴标截止时间</span><el-date-picker v-model="form.labelDeadline" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="" /></label>
|
||
<label v-if="props.service === 'labeling' && labelingMode !== 'sku'"><span><i>*</i>换标后派送方式</span><el-select v-model="form.deliveryMethod" placeholder=""><el-option label="卡车派送" value="TRUCK" /><el-option label="快递派送" value="EXPRESS" /><el-option label="亚马逊自提" value="AMAZON_PICKUP" /><el-option label="客户自提" value="CUSTOMER_PICKUP" /></el-select></label>
|
||
<div v-if="props.service === 'labeling' && labelingMode !== 'sku' && form.deliveryMethod === 'EXPRESS'" class="warehouse-checkbox-field"><span>是否打印面单</span><el-checkbox v-model="form.relabel">需要仓库打印面单</el-checkbox></div>
|
||
<label v-if="canManageRelabelFiles && props.service === 'labeling' && labelingMode !== 'sku' && form.deliveryMethod === 'EXPRESS' && form.relabel"><span><i>*</i>上传面单文件</span><input class="warehouse-native-file" type="file" accept="application/pdf,image/*,.pdf" @change="selectShippingLabel"></label>
|
||
<label v-if="props.service === 'relabel'" class="warehouse-span-2"><span><i>*</i>详细派送地址</span><el-input v-model="form.address" placeholder="" /></label>
|
||
<label v-if="props.service === 'relabel'"><span><i>*</i>预估总体积(CBM)</span><el-input-number v-model="form.volumeCbm" :min="0" :precision="4" :step="0.0001" controls-position="right" placeholder="" /></label>
|
||
<label v-if="(props.service === 'transfer' && !inventoryTransfer) || props.service === 'relabel'"><span><i>*</i>{{ props.service === 'transfer' ? '预计入仓时间' : '预计到达时间' }}</span><el-date-picker v-model="form.estimatedArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" :placeholder="props.service === 'transfer' ? '' : '请选择预计到达时间'" /></label>
|
||
<label v-if="props.service === 'transfer'"><span>期望发货时间</span><el-date-picker v-model="form.estimatedShipAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="" /></label>
|
||
<div v-if="props.service === 'transfer'" class="warehouse-choice-field warehouse-span-2"><span>目的去向</span><el-radio-group v-model="transferDestinationType" @change="selectTransferDestinationType"><el-radio-button v-if="!inventoryTransfer" value="NONE">暂未确定</el-radio-button><el-radio-button value="FBA">FBA 仓</el-radio-button><el-radio-button value="PRIVATE">私人仓</el-radio-button></el-radio-group></div>
|
||
<label v-if="props.service === 'transfer' && transferDestinationType === 'FBA'"><span><i>*</i>FBA 目的仓</span><el-select v-model="form.destination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder=""><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select></label>
|
||
<label v-if="props.service === 'transfer' && transferDestinationType === 'FBA'"><span>FBA 货件编号</span><el-input v-model="form.fbaShipmentNo" placeholder="" /></label>
|
||
<label v-if="props.service === 'transfer' && transferDestinationType === 'PRIVATE'"><span><i>*</i>私人仓地址</span><el-input v-model="form.privateDestinationAddress" maxlength="1000" placeholder="" /></label>
|
||
<label v-if="props.service === 'transfer' && transferDestinationType === 'PRIVATE'"><span><i>*</i>邮编</span><el-input v-model="form.privateDestinationPostalCode" maxlength="32" placeholder="" /></label>
|
||
<label v-if="props.service === 'transfer' && transferDestinationType === 'PRIVATE'"><span><i>*</i>私人仓联系人</span><el-input v-model="form.privateDestinationContact" maxlength="128" placeholder="" /></label>
|
||
<label v-if="props.service === 'transfer' && transferDestinationType === 'PRIVATE'"><span><i>*</i>联系电话</span><el-input v-model="form.privateDestinationPhone" maxlength="64" placeholder="" /></label>
|
||
<label v-if="props.service === 'relabel'"><span><i>*</i>FBA 货件编号</span><el-input v-model="form.fbaShipmentNo" placeholder="" /></label>
|
||
<label v-if="props.service === 'labeling'"><span>贴标要求</span><el-input v-model="form.requirement" placeholder="" /></label>
|
||
<label v-else-if="props.service === 'relabel' || (props.service === 'transfer' && form.deliveryMethod === 'CUSTOMER_DELIVERY')"><span>物流跟踪号</span><el-input v-model="form.trackingNo" :placeholder="props.service === 'transfer' ? '' : '请输入物流跟踪号(选填)'" /></label>
|
||
<label class="warehouse-span-2"><span>{{ props.service === 'disposal' && discardQuantityMode ? '备注 *' : '备注' }}</span><el-input v-model="form.remark" type="textarea" :rows="2" placeholder="" /></label>
|
||
<div v-if="props.service === 'transfer'" class="warehouse-choice-field warehouse-choice-field--relabel warehouse-span-2"><span><i>*</i>是否贴标</span><el-radio-group v-model="form.relabel"><el-radio-button v-if="!transferRelabelRequired" :value="false">不需要贴标</el-radio-button><el-radio-button :value="true">需要贴标</el-radio-button></el-radio-group></div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
<div class="warehouse-card warehouse-form-section warehouse-transfer-cargo-card">
|
||
<template v-if="props.service === 'labeling'">
|
||
<div class="warehouse-section-heading warehouse-labeling-heading">
|
||
<div><h3>货物明细</h3></div>
|
||
</div>
|
||
<div class="warehouse-labeling-body">
|
||
<div class="warehouse-file-groups warehouse-file-groups--compact">
|
||
<section v-if="canManageRelabelFiles" class="warehouse-file-group warehouse-file-group--primary">
|
||
<div class="warehouse-file-group__heading"><div><strong><span>1</span>贴标总文件</strong></div><em>{{ requiresBothTotalFiles ? '必传 · 两份文件' : isReturnRelabelSource ? labelingMode === 'sku' ? '必传 · SKU 标签文件' : '必传 · 箱唛文件' : '必传 · 二选一' }}</em></div>
|
||
<div class="warehouse-file-options">
|
||
<div class="warehouse-file-option" :class="{ 'is-selected': Boolean(cartonTotalFile) }"><div class="warehouse-file-option__name"><strong>箱唛总文件</strong></div><el-button native-type="button" :type="cartonTotalFile ? 'primary' : 'default'" plain @click="chooseCreateTotalFile('CARTON_TOTAL')"><Upload :size="15" />{{ cartonTotalFile ? '重新上传' : '选择文件' }}</el-button><div class="warehouse-file-option__status" :class="{ 'is-empty': !cartonTotalFile }"><span>{{ cartonTotalFile?.name || '尚未选择文件' }}</span><button v-if="cartonTotalFile" type="button" class="warehouse-file-clear" title="清除箱唛总文件" aria-label="清除箱唛总文件" :disabled="saving" @click="clearCreateTotalFile('CARTON_TOTAL')"><X :size="14" /></button></div></div>
|
||
<div class="warehouse-file-choice-or" :class="{ 'is-both': requiresBothTotalFiles }">{{ requiresBothTotalFiles ? '和' : '或' }}</div>
|
||
<div class="warehouse-file-option" :class="{ 'is-selected': Boolean(skuTotalFile) }"><div class="warehouse-file-option__name"><strong>SKU 总文件</strong></div><el-button native-type="button" :type="skuTotalFile ? 'primary' : 'default'" plain @click="chooseCreateTotalFile('SKU_TOTAL')"><Upload :size="15" />{{ skuTotalFile ? '重新上传' : '选择文件' }}</el-button><div class="warehouse-file-option__status" :class="{ 'is-empty': !skuTotalFile }"><span>{{ skuTotalFile?.name || '尚未选择文件' }}</span><button v-if="skuTotalFile" type="button" class="warehouse-file-clear" title="清除 SKU 总文件" aria-label="清除 SKU 总文件" :disabled="saving" @click="clearCreateTotalFile('SKU_TOTAL')"><X :size="14" /></button></div></div>
|
||
</div>
|
||
</section>
|
||
<section class="warehouse-file-group">
|
||
<div class="warehouse-file-group__heading"><div><strong><span>2</span>明细关联模板</strong></div><em>按需使用</em></div>
|
||
<div class="warehouse-template-list">
|
||
<div><span><strong>统一贴标明细</strong></span><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate()"><Download :size="15" />下载模板</el-button><el-button type="primary" plain size="small" :disabled="!cargoRows.length" @click="unifiedTemplateInput?.click()"><Upload :size="15" />上传文件</el-button></div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
<input ref="unifiedTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate"><input ref="cartonTotalCreateInput" class="warehouse-hidden-input" type="file" accept="application/pdf,.pdf" @change="selectCartonTotalFile"><input ref="skuTotalCreateInput" class="warehouse-hidden-input" type="file" accept="application/pdf,.pdf" @change="selectSkuTotalFile">
|
||
<div v-if="isReturnRelabelSource" class="warehouse-table-scroll">
|
||
<el-input v-model="returnCartonQuery" clearable placeholder="" />
|
||
<template v-if="returnLooseCandidates.length">
|
||
<div class="warehouse-section-heading"><h3>可用包裹</h3><el-button text type="primary" native-type="button" :aria-expanded="!availableSkusCollapsed" aria-controls="return-available-skus" @click="availableSkusCollapsed = !availableSkusCollapsed">{{ availableSkusCollapsed ? '展开明细' : '收起明细' }}</el-button></div>
|
||
<table v-show="!availableSkusCollapsed" id="return-available-skus" class="warehouse-edit-table"><thead><tr><th>包裹识别码</th><th>来源退货单</th><th>SKU</th><th>SKU 换标结果</th><th>剩余可换(件)</th><th>操作</th></tr></thead><tbody><template v-for="source in returnLooseCandidates" :key="source.returnCartonId"><tr v-for="sku in source.skuItems" :key="sku.skuCode"><td>{{ source.cartonMark }}</td><td>{{ source.inboundOrderNo }}</td><td>{{ sku.skuCode }}</td><td><div v-for="conversion in (source.skuConversions || []).filter(item => item.status !== 'REVERSED')" :key="`${conversion.relabelNo}-${conversion.oldSkuCode}-${conversion.newSkuCode}`">{{ conversion.oldSkuCode }} → {{ conversion.newSkuCode }}:已换 {{ conversion.quantity }} 件{{ conversion.status === 'APPLIED' ? '(待确认)' : '' }}</div><span v-if="!source.skuConversions?.some(item => item.status !== 'REVERSED')">尚未换标</span></td><td>{{ sku.quantity }}</td><td><el-button size="small" :disabled="inventoryQuantity(source.availablePackages) <= 0" @click="labelingMode === 'sku' ? addReturnSkuBinding(source, sku.skuCode) : addReturnBoxBinding(source)">添加贴标</el-button></td></tr></template><tr v-if="!returnLooseCandidates.length"><td colspan="6">暂无可用包裹库存</td></tr></tbody></table>
|
||
</template>
|
||
<template v-if="returnBoxCandidates.length">
|
||
<div class="warehouse-section-heading"><h3>可用箱子({{ returnBoxCandidates.length }} 箱)</h3><el-button text type="primary" native-type="button" :aria-expanded="!availableBoxesCollapsed" aria-controls="return-available-boxes" @click="availableBoxesCollapsed = !availableBoxesCollapsed">{{ availableBoxesCollapsed ? '展开明细' : '收起明细' }}</el-button></div>
|
||
<table v-show="!availableBoxesCollapsed" id="return-available-boxes" class="warehouse-edit-table"><thead><tr><th>仓库箱识别码</th><th>来源退货单</th><th>箱内 SKU</th><th>SKU 换标结果</th><th>剩余可换(件)</th><th>操作</th></tr></thead><tbody><tr v-for="source in returnBoxCandidates" :key="source.returnCartonId"><td><strong>{{ source.cartonMark }}</strong></td><td>{{ source.inboundOrderNo }}</td><td><div v-for="sku in source.skuItems" :key="sku.skuCode">{{ sku.skuCode }}</div></td><td><div v-for="conversion in (source.skuConversions || []).filter(item => item.status !== 'REVERSED')" :key="`${conversion.relabelNo}-${conversion.oldSkuCode}-${conversion.newSkuCode}`">{{ conversion.oldSkuCode }} → {{ conversion.newSkuCode }}:已换 {{ conversion.quantity }} 件{{ conversion.status === 'APPLIED' ? '(待确认)' : '' }}</div><span v-if="!source.skuConversions?.some(item => item.status !== 'REVERSED')">尚未换标</span></td><td><div v-for="sku in source.skuItems" :key="sku.skuCode">{{ sku.quantity }}</div></td><td><el-button size="small" :disabled="inventoryQuantity(source.availablePackages) <= 0" @click="addReturnBoxBinding(source)">添加贴标</el-button></td></tr><tr v-if="!returnBoxCandidates.length"><td colspan="6">暂无可用箱子库存</td></tr></tbody></table>
|
||
</template>
|
||
</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>
|
||
</div>
|
||
<div v-if="importedRelabelBindings.length" class="warehouse-import-preview">
|
||
<div class="warehouse-section-heading"><div><strong>待导入绑定关系</strong></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></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></tr></tbody></table></div>
|
||
</div>
|
||
<div class="warehouse-section-heading warehouse-tab-actions">
|
||
<div><strong>绑定明细</strong></div>
|
||
<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>
|
||
<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>
|
||
<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>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in cartonDetailRows" :key="row.id"><td><el-input v-model="row.sku" placeholder="请输入 SKU" /></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" :readonly="isReturnRelabelSource" placeholder="请输入仓库唛头" /></td><td><el-input v-model="row.fbaCartonMark" placeholder="请输入新 FBA 箱唛" /></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="8"><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" :readonly="isReturnRelabelSource" placeholder="请输入仓库唛头" @change="applyExistingCartonBinding(row)" /></td><td><el-input v-model="row.originalSku" :readonly="isReturnRelabelSource" 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-input v-model="row.shipmentNo" placeholder="请输入 FBA 货件编号" /></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">暂无绑定明细</div></td></tr></tbody></table></div>
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<div v-if="isTransferCreate || isDisposalCreate" class="warehouse-order-info-heading"><h3>{{ isDisposalCreate && discardQuantityMode ? '废弃数量' : isDisposalCreate && discardSelectedCandidate?.cartonSelectionSupported ? '选择废弃箱子' : '货物明细' }}</h3></div>
|
||
<div :class="{ 'warehouse-transfer-create-body': isTransferCreate, 'warehouse-disposal-create-body': isDisposalCreate }">
|
||
<section v-if="props.service === 'disposal' && discardSelectedCandidate" class="warehouse-discard-boxes">
|
||
<div class="warehouse-section-heading"><div><h3>{{ discardQuantityMode ? '废弃数量' : '选择废弃箱子' }}</h3></div><div v-if="discardSelectedCandidate.cartonSelectionSupported" class="warehouse-heading-actions"><el-button size="small" @click="selectAllDiscardBoxes(true)">全选</el-button><el-button size="small" @click="selectAllDiscardBoxes(false)">清空选择</el-button></div></div>
|
||
<div v-if="isDisposalCreate && discardQuantityMode" class="warehouse-form-grid warehouse-disposal-quantity">
|
||
<el-form-item label="本次废弃箱数" prop="requestedPackages" :rules="[{ required: true, type: 'integer', min: 1, max: Number(discardSelectedCandidate.availablePackages), message: '请填写可用范围内的整数箱数', trigger: 'change' }]"><el-input-number v-model="discardRequestedPackages" :min="1" :max="Number(discardSelectedCandidate.availablePackages)" :precision="0" controls-position="right" placeholder="请输入废弃箱数" /></el-form-item>
|
||
<span class="warehouse-hint">可用 {{ discardSelectedCandidate.availablePackages }} 箱</span>
|
||
</div>
|
||
<div v-else-if="discardQuantityMode" class="warehouse-disposal-field"><span>本次废弃箱数 *</span><el-input-number v-model="discardRequestedPackages" :min="1" :max="Number(discardSelectedCandidate.availablePackages)" :precision="0" controls-position="right" placeholder="请输入废弃箱数" /><span>可用 {{ discardSelectedCandidate.availablePackages }} 箱</span></div>
|
||
<template v-else-if="discardSelectedCandidate.cartonSelectionSupported">
|
||
<div class="warehouse-discard-box-toolbar"><el-input v-model="discardBoxSearch" clearable :placeholder="isDisposalCreate ? '搜索箱唛或 SKU' : ''" /><strong>已选 {{ discardSelectedBoxKeys.length }} / {{ discardSelectableBoxes.length }} 箱</strong></div>
|
||
<div class="warehouse-table-scroll"><table class="warehouse-edit-table"><thead><tr><th>选择</th><th>箱唛</th><th>所属托盘</th><th>箱内 SKU</th><th>中文品名</th></tr></thead><tbody><tr v-for="box in discardVisibleBoxes" :key="box.key"><td><el-checkbox :model-value="discardSelectedBoxKeys.includes(box.key)" :aria-label="`选择废弃箱子 ${box.cartonMark}`" @change="toggleDiscardBox(box.key, Boolean($event))" /></td><td><strong>{{ box.cartonMark }}</strong></td><td>{{ box.palletNo || '散箱' }}</td><td>{{ box.skus.map(sku => sku.skuCode).join('、') }}</td><td>{{ box.skus.map(sku => sku.productName || '—').join('、') }}</td></tr><tr v-if="!discardVisibleBoxes.length"><td colspan="5"><div class="warehouse-empty">暂无匹配箱唛</div></td></tr></tbody></table></div>
|
||
</template>
|
||
<el-alert v-else type="info" :closable="false" :title="discardSelectedCandidate.cartonSelectionBlockReason || '当前库存缺少箱唛与托盘对应关系,将预留所选库存,由仓库核对相关托盘并填写实际废弃箱数,未废弃部分释放。'" />
|
||
<div v-if="!discardQuantityMode && !discardSelectedCandidate.cartonSelectionSupported" class="warehouse-hint">相关托盘:{{ [...new Set(discardSelectionSources.map(source => source.palletNo).filter(Boolean))].join('、') || '散箱/托盘信息未记录' }}</div>
|
||
</section>
|
||
<section v-if="inventoryTransfer" v-loading="transferSourceLoading">
|
||
<div class="warehouse-section-heading"><h3>原单箱明细 <small>共 {{ transferSourceCartons.length }} 箱 · 只读</small></h3></div>
|
||
<el-alert v-if="transferSourceError" :title="transferSourceError" type="error" :closable="false"><el-button link @click="loadTransferSourceCartons">重新加载</el-button></el-alert>
|
||
<div class="warehouse-table-scroll"><table class="warehouse-edit-table"><thead><tr><th>箱号</th><th>商品 SKU</th><th>中文品名</th><th>箱内数量(件)</th><th>原目的仓</th></tr></thead><tbody>
|
||
<tr v-for="box in transferSourceCartons" :key="box.cartonMark"><td>{{ box.cartonMark }}</td><td>{{ box.items.map(item => item.skuCode || '—').join(' / ') }}</td><td>{{ box.productNameZh || '—' }}</td><td>{{ box.quantity ?? '未记录' }}</td><td>{{ [box.destinationWarehouseCode, box.destinationWarehouseName].filter(Boolean).join(' · ') || '未指定' }}</td></tr>
|
||
<tr v-if="!transferSourceLoading && !transferSourceCartons.length"><td colspan="5"><div class="warehouse-empty">{{ transferSourceError ? '加载失败,请重试' : '当前来源单暂无未换标的箱明细' }}</div></td></tr>
|
||
</tbody></table></div>
|
||
<div v-if="!transferSourceLoading && !transferSourceError && !transferSourceCartons.length" class="warehouse-table-scroll"><table class="warehouse-edit-table"><thead><tr><th>商品 SKU</th><th>中文品名</th><th>库存箱数</th><th>原目的仓</th></tr></thead><tbody><tr v-for="row in cargoRows" :key="row.id"><td>{{ row.sku || '—' }}</td><td>{{ row.name || '—' }}</td><td>{{ row.expectedPackageCount }}</td><td>{{ transferInventoryCandidates.filter(item => item.inboundCode === selectedTransferInventory && item.skuCode === row.sku).map(item => item.destinationFbaWarehouseCode || item.destinationFbaWarehouseName).filter(Boolean).join(' / ') || '未指定' }}</td></tr></tbody></table></div>
|
||
</section>
|
||
<div v-if="!inventoryTransfer && (props.service !== 'disposal' || (!discardQuantityMode && !discardSelectedCandidate?.cartonSelectionSupported))" class="warehouse-section-heading" :class="{ 'warehouse-step-heading': props.service === 'transfer' }"><div><h3><span v-if="props.service === 'transfer'" class="warehouse-step-number">1</span>货物明细</h3></div><div v-if="props.service !== 'disposal'" class="warehouse-heading-actions"><el-button v-if="props.service === 'transfer'" size="small" :loading="saving" @click="printForecastCartons"><Printer :size="15" />打印唛头</el-button><el-button type="primary" size="small" @click="addCargoRow"><Plus :size="15" />添加行</el-button><el-button size="small" @click="cargoRows = []">清空</el-button></div></div>
|
||
<div v-if="!inventoryTransfer && (props.service !== 'disposal' || (!discardQuantityMode && !discardSelectedCandidate?.cartonSelectionSupported))" class="warehouse-table-scroll"><table class="warehouse-edit-table"><thead><tr><th>商品 SKU{{ props.service === 'transfer' ? '' : ' *' }}</th><th>中文品名{{ props.service === 'transfer' ? '' : ' *' }}</th><th v-if="props.service === 'disposal'">可用箱数</th><th v-if="props.service === 'relabel'">FNSKU / 箱唛</th><th v-if="props.service === 'transfer'">SKU 数量(件)*</th><th>{{ props.service === 'disposal' ? '本次废弃箱数' : '预计箱数' }} *</th><th v-if="props.service !== 'disposal'">操作</th></tr></thead><tbody><tr v-for="row in cargoRows" :key="row.id"><td><el-input v-model="row.sku" :readonly="props.service === 'disposal'" :placeholder="isTransferCreate ? '请输入商品 SKU(选填)' : props.service === 'relabel' ? '请输入商品 SKU' : ''" /></td><td><el-input v-model="row.name" :readonly="props.service === 'disposal'" :placeholder="isTransferCreate ? '请输入中文品名(选填)' : props.service === 'relabel' ? '请输入中文品名' : ''" /></td><td v-if="props.service === 'disposal'"><strong>{{ row.availableQuantity }} 箱</strong></td><td v-if="props.service === 'relabel'"><el-input v-model="row.shippingMark" placeholder="请输入FNSKU或箱唛" /></td><td v-if="props.service === 'transfer'"><el-input-number v-model="row.quantity" :min="1" :precision="0" controls-position="right" placeholder="" /></td><td v-if="props.service === 'transfer'"><el-input-number v-model="row.expectedPackageCount" :min="1" :precision="0" controls-position="right" placeholder="" /></td><td v-else><el-input-number v-model="row.quantity" :disabled="props.service === 'disposal'" :min="props.service === 'disposal' ? 0 : 1" :max="row.availableQuantity" controls-position="right" :placeholder="props.service === 'relabel' ? '请输入箱数' : ''" /></td><td v-if="props.service !== 'disposal'"><button type="button" class="warehouse-delete" title="删除" @click="removeCargoRow(row.id)"><Trash2 :size="16" />删除</button></td></tr><tr v-if="cargoRows.length === 0"><td :colspan="props.service === 'disposal' ? 4 : props.service === 'relabel' || props.service === 'transfer' ? 5 : 4"><div class="warehouse-empty">暂无货物明细</div></td></tr></tbody></table></div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
<TransferPaneDivider v-if="props.service === 'transfer' && form.relabel" v-model="transferPaneShare" class="warehouse-transfer-divider" />
|
||
<div id="transfer-relabel-pane" v-if="props.service === 'transfer' && form.relabel" class="warehouse-card warehouse-form-section warehouse-transfer-relabel">
|
||
<div class="warehouse-section-heading warehouse-labeling-heading warehouse-step-heading"><div><h3><span class="warehouse-step-number">2</span>换标申请</h3></div></div>
|
||
<div :class="{ 'warehouse-transfer-create-body': isTransferCreate, 'warehouse-disposal-create-body': isDisposalCreate }">
|
||
<div v-if="isTransferCreate" class="warehouse-form-grid warehouse-transfer-relabel-fields">
|
||
<el-form-item label="换标截止时间" prop="labelDeadline"><el-date-picker v-model="form.labelDeadline" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择换标截止时间" popper-class="transfer-create-popper" /></el-form-item>
|
||
<el-form-item label="换标联系人" prop="relabelContact"><el-input v-model="form.relabelContact" maxlength="128" placeholder="请输入换标联系人" /></el-form-item>
|
||
<el-form-item label="换标联系人电话" prop="relabelPhone"><el-input v-model="form.relabelPhone" type="tel" maxlength="64" placeholder="请输入换标联系电话" /></el-form-item>
|
||
<el-form-item v-if="labelingMode !== 'sku'" label="换标后派送方式" prop="relabelDeliveryMethod"><el-select v-model="form.relabelDeliveryMethod" placeholder="请选择派送方式" popper-class="transfer-create-popper"><el-option label="卡车派送" value="TRUCK" /><el-option label="快递派送" value="EXPRESS" /><el-option label="亚马逊自提" value="AMAZON_PICKUP" /><el-option label="客户自提" value="CUSTOMER_PICKUP" /></el-select></el-form-item>
|
||
<el-form-item class="warehouse-span-2" label="整票换标货物目的 FBA 仓" prop="relabelDestination"><el-select v-model="transferRelabelDefaultDestination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder="请选择整票换标货物目的 FBA 仓" popper-class="transfer-create-popper"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select></el-form-item>
|
||
<el-form-item label="FBA 货件编号"><el-input v-model="form.fbaShipmentNo" placeholder="请输入 FBA 货件编号(选填)" /></el-form-item>
|
||
<el-form-item class="warehouse-span-2" label="贴标要求"><el-input v-model="form.requirement" type="textarea" :rows="3" maxlength="1000" show-word-limit placeholder="请输入贴标要求(选填)" /></el-form-item>
|
||
</div>
|
||
<div v-else 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.relabelContact" placeholder="" maxlength="128" /></label><label><span><i>*</i>换标联系人电话</span><el-input v-model="form.relabelPhone" placeholder="" maxlength="64" /></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="AMAZON_PICKUP" /><el-option label="客户自提" value="CUSTOMER_PICKUP" /></el-select></label><label class="warehouse-span-2"><span><i>*</i>整票换标货物目的 FBA 仓</span><el-select v-model="transferRelabelDefaultDestination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder=""><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></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></div>
|
||
<div class="warehouse-transfer-file-groups warehouse-file-groups--compact">
|
||
<section v-if="canManageRelabelFiles" class="warehouse-file-group warehouse-transfer-total-files">
|
||
<div class="warehouse-file-group__heading"><div><strong>贴标总文件</strong></div><em>{{ requiresBothTotalFiles ? '必传 · 两份文件' : '必传 · 二选一' }}</em></div>
|
||
<div class="warehouse-file-options">
|
||
<div class="warehouse-file-option" :class="{ 'is-selected': Boolean(cartonTotalFile) }"><div class="warehouse-file-option__name"><strong>箱唛总文件</strong></div><el-button native-type="button" :type="cartonTotalFile ? 'primary' : 'default'" plain @click="chooseCreateTotalFile('CARTON_TOTAL')"><Upload :size="15" />{{ cartonTotalFile ? '重新上传' : '选择文件' }}</el-button><div class="warehouse-file-option__status" :class="{ 'is-empty': !cartonTotalFile }"><span>{{ cartonTotalFile?.name || '尚未选择文件' }}</span><button v-if="cartonTotalFile" type="button" class="warehouse-file-clear" title="清除箱唛总文件" aria-label="清除箱唛总文件" :disabled="saving" @click="clearCreateTotalFile('CARTON_TOTAL')"><X :size="14" /></button></div></div>
|
||
<div class="warehouse-file-choice-or" :class="{ 'is-both': requiresBothTotalFiles }">{{ requiresBothTotalFiles ? '和' : '或' }}</div>
|
||
<div class="warehouse-file-option" :class="{ 'is-selected': Boolean(skuTotalFile) }"><div class="warehouse-file-option__name"><strong>SKU 总文件</strong></div><el-button native-type="button" :type="skuTotalFile ? 'primary' : 'default'" plain @click="chooseCreateTotalFile('SKU_TOTAL')"><Upload :size="15" />{{ skuTotalFile ? '重新上传' : '选择文件' }}</el-button><div class="warehouse-file-option__status" :class="{ 'is-empty': !skuTotalFile }"><span>{{ skuTotalFile?.name || '尚未选择文件' }}</span><button v-if="skuTotalFile" type="button" class="warehouse-file-clear" title="清除 SKU 总文件" aria-label="清除 SKU 总文件" :disabled="saving" @click="clearCreateTotalFile('SKU_TOTAL')"><X :size="14" /></button></div></div>
|
||
</div>
|
||
<input ref="cartonTotalCreateInput" class="warehouse-hidden-input" type="file" accept="application/pdf,.pdf" @change="selectCartonTotalFile"><input ref="skuTotalCreateInput" class="warehouse-hidden-input" type="file" accept="application/pdf,.pdf" @change="selectSkuTotalFile">
|
||
</section>
|
||
<section class="warehouse-file-group warehouse-transfer-template-group">
|
||
<div class="warehouse-file-group__heading"><div><strong><span>3</span>明细关联模板</strong></div><em>按需使用</em></div><div class="warehouse-template-list"><div><span><strong>统一贴标明细</strong></span><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate()"><Download :size="15" />下载模板</el-button><el-button type="primary" plain size="small" :disabled="!cargoRows.length" @click="unifiedTemplateInput?.click()"><Upload :size="15" />上传文件</el-button></div></div>
|
||
<input ref="unifiedTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate">
|
||
</section>
|
||
</div>
|
||
<div v-if="importedRelabelBindings.length" class="warehouse-import-preview"><div class="warehouse-section-heading"><div><strong>待导入绑定关系</strong></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></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></tr></tbody></table></div></div>
|
||
<div class="warehouse-section-heading warehouse-tab-actions warehouse-binding-heading"><div><strong><span class="warehouse-step-number">4</span>绑定明细</strong></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>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in transferRelabelRows" :key="row.id"><td><el-input v-model="row.customerCartonMarks" :placeholder="isTransferCreate ? '请输入仓库唛头' : ''" /></td><td><el-input :model-value="row.sku" readonly /></td><td><el-input v-model="row.fbaCartonMark" :placeholder="isTransferCreate ? '请输入新 FBA 箱唛' : ''" /></td><td><el-input v-model="row.remark" type="textarea" :rows="2" :placeholder="isTransferCreate ? '选填' : ''" /></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="5"><div class="warehouse-empty">暂无绑定明细</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" :placeholder="isTransferCreate ? '请输入仓库唛头' : ''" /></td><td><el-input :model-value="row.sku" readonly /></td><td><el-input v-model="row.newSkuCode" :placeholder="isTransferCreate ? '请输入新 SKU' : ''" /></td><td><el-input-number v-model="row.quantity" :min="1" :precision="0" controls-position="right" :placeholder="isTransferCreate ? '请输入数量' : ''" /></td><td><el-input v-model="row.remark" type="textarea" :rows="2" :placeholder="isTransferCreate ? '选填' : ''" /></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">暂无绑定明细</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>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in transferRelabelRows" :key="row.id"><td><el-input v-model="row.customerCartonMarks" :placeholder="isTransferCreate ? '请输入仓库唛头' : ''" /></td><td><el-input :model-value="row.sku" readonly /></td><td><el-input v-model="row.newSkuCode" :placeholder="isTransferCreate ? '请输入新 SKU' : ''" /></td><td><el-input-number v-model="row.quantity" :min="1" :precision="0" controls-position="right" :placeholder="isTransferCreate ? '请输入数量' : ''" /></td><td><el-input v-model="row.fbaCartonMark" :placeholder="isTransferCreate ? '请输入新 FBA 箱唛' : ''" /></td><td><el-input v-model="row.remark" type="textarea" :rows="2" :placeholder="isTransferCreate ? '选填' : ''" /></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="7"><div class="warehouse-empty">暂无绑定明细</div></td></tr></tbody></table></div>
|
||
</div>
|
||
</div>
|
||
<div class="warehouse-submit-bar warehouse-transfer-submit-bar"><el-button v-if="isServiceCreate" native-type="button" :disabled="saving" @click="returnToList">返回列表</el-button><el-button v-if="mode === 'create'" native-type="button" @click="resetForm">重置</el-button><el-button v-if="props.service === 'transfer' && !inventoryTransfer" native-type="button" :loading="saving" @click="submit('draft')">保存草稿</el-button><el-button v-if="props.service === 'transfer'" type="primary" native-type="submit" :loading="saving">提交订单</el-button><el-button v-if="props.service !== 'transfer'" type="primary" native-type="submit" :loading="saving">提交{{ config.title }}</el-button></div>
|
||
</component>
|
||
</template>
|
||
|
||
<template v-else>
|
||
<header class="warehouse-detail-header">
|
||
<button type="button" class="warehouse-detail-back" @click="returnToList"><ArrowLeft :size="16" />返回列表</button>
|
||
<div class="warehouse-detail-title"><strong>{{ props.service === 'labeling' ? selectedRelabelDetail?.application.relabelNo : selectedDetail?.transferNo || selectedDetail?.['0'] || `${config.title}详情` }}</strong></div>
|
||
<div class="warehouse-detail-actions"><el-button v-if="canCreate && props.service === 'labeling' && selectedRelabelDetail?.application.status === 'REJECTED'" type="primary" @click="resubmitRejectedRelabel">修改后重新提交</el-button><el-button v-if="canConfirmRelabel && props.service === 'labeling' && selectedRelabelDetail?.application.status === 'CUSTOMER_CONFIRM_PENDING' && !selectedRelabelDetail?.application.customerConfirmedAt" type="primary" @click="confirmSelectedRelabel">确认换标完成</el-button><el-button v-if="canConfirmDiscard && props.service === 'disposal' && selectedDetail?.status === 'PDA_COMPLETED'" type="primary" @click="openCompletionConfirmation(String(selectedDetail?.discardNo || ''), 'disposal')">废弃确认</el-button><el-button v-if="canCancelDiscard && props.service === 'disposal' && selectedDetail?.status === 'PENDING_REVIEW'" type="danger" @click="cancelSelectedDiscard">取消申请</el-button><el-button v-if="canCreate && props.service === 'transfer' && ['DRAFT', 'REJECTED'].includes(selectedDetail?.status || '')" type="primary" @click="openEdit(String(selectedDetail?.transferNo || selectedDetail?.['0'] || ''))">编辑</el-button><el-button v-if="canConfirmTransfer && props.service === 'transfer' && selectedDetail?.status === 'WAIT_CUSTOMER_CONFIRMATION'" type="primary" @click="confirmTransferMeasurements">确认测量数据</el-button><el-tag v-if="props.service !== 'transfer'" :type="props.service === 'labeling' ? relabelStatusTagType(selectedRelabelDetail?.application.status || '') : undefined" effect="light">{{ statusLabel(props.service === 'labeling' ? selectedRelabelDetail?.application.status || '' : String(selectedDetail?.status || '')) }}</el-tag></div>
|
||
</header>
|
||
<div v-if="detailError" class="warehouse-detail-error" role="alert"><strong>{{ detailError }}</strong><el-button @click="loadDetailPage">重新加载</el-button></div>
|
||
<ReviewRejectionNotice v-if="!detailError" :status="props.service === 'labeling' ? selectedRelabelDetail?.application.status : selectedDetail?.status" :reason="props.service === 'labeling' ? selectedRelabelDetail?.application.reviewRemark : selectedDetail?.reviewRemark" />
|
||
<div v-if="!detailError" class="warehouse-card warehouse-form-section"><h3>订单概览</h3><div v-if="props.service === 'labeling'" class="warehouse-detail-grid"><div><span>操作仓</span><strong>{{ warehouseDisplay(selectedRelabelDetail?.application.overseasWarehouseId) }}</strong></div><div><span>关联单号</span><strong>{{ selectedRelabelDetail?.application.customerOrderNo || '—' }}</strong></div><div><span>SKU 数(种)</span><strong>{{ selectedRelabelDetail ? relabelSkuCount(selectedRelabelDetail.items) : '—' }}</strong></div><div><span>箱数</span><strong :class="{ 'warehouse-missing-value': selectedRelabelQuantity?.cartonCount == null }">{{ selectedRelabelQuantity?.cartonCount ?? '—' }}</strong></div><div><span>贴标截止时间</span><strong>{{ formatDateTime(selectedRelabelDetail?.application.labelDeadline) }}</strong></div><div><span>仓库联系人</span><strong>{{ selectedRelabelDetail?.application.contactName || '—' }}</strong></div><div><span>联系电话</span><strong>{{ selectedRelabelDetail?.application.contactPhone || '—' }}</strong></div><div><span>创建时间</span><strong>{{ formatDateTime(selectedRelabelDetail?.application.createdAt) }}</strong></div></div><div v-else-if="props.service === 'transfer'" class="warehouse-detail-grid warehouse-transfer-overview-grid"><div><span>状态</span><el-tag :type="transferStatusTagType(selectedDetail?.status || '')" effect="light">{{ statusLabel(selectedDetail?.status || '') }}</el-tag></div><div><span>目的去向</span><strong>{{ destinationTypeLabel(selectedDetail?.destinationType, selectedDetail?.fbaWarehouseCode) }}</strong></div><div v-if="selectedDetail?.destinationType !== 'PRIVATE'"><span>FBA 目的仓</span><strong>{{ selectedDetail?.destinationWarehouseName || warehouseDisplay(selectedDetail?.destinationWarehouseCode || selectedDetail?.fbaWarehouseCode) }}</strong></div><div v-if="selectedDetail?.destinationType !== 'PRIVATE'"><span>FBA 货件号</span><strong>{{ selectedDetail?.fbaShipmentNo || '—' }}</strong></div><div v-if="selectedDetail?.destinationType === 'PRIVATE'" class="warehouse-overview-wide"><span>私人仓地址</span><strong>{{ selectedDetail?.privateDestinationAddress || '—' }}</strong></div><div v-if="selectedDetail?.destinationType === 'PRIVATE'"><span>邮编</span><strong>{{ selectedDetail?.privateDestinationPostalCode || '—' }}</strong></div><div v-if="selectedDetail?.destinationType === 'PRIVATE'"><span>私人仓联系人</span><strong>{{ selectedDetail?.privateDestinationContactName || '—' }}</strong></div><div v-if="selectedDetail?.destinationType === 'PRIVATE'"><span>联系电话</span><strong>{{ selectedDetail?.privateDestinationContactPhone || '—' }}</strong></div><div><span>海外仓</span><strong>{{ warehouseDisplay(selectedDetail?.operatingWarehouseId) }}</strong></div><div><span>预报 SKU 种数</span><strong>{{ new Set((selectedDetail?.items || []).map(item => item.skuCode).filter(Boolean)).size }}</strong></div><div><span>预报 SKU 总件数</span><strong>{{ transferForecastQuantity }}</strong></div><div><span>预计箱数</span><strong>{{ selectedDetail?.packageCount ?? '—' }}</strong></div><div><span>实际签收箱数</span><strong>{{ countedTransferCartons().length ?? '—' }}</strong></div><div><span>入仓方式</span><strong>{{ deliveryMethodLabel(selectedDetail?.deliveryMethod || '') }}</strong></div><div><span>期望发货时间</span><strong>{{ formatDateTime(selectedDetail?.estimatedShipAt) }}</strong></div><div><span>实际发货时间</span><strong>{{ formatDateTime(selectedDetail?.actualShipAt) }}</strong></div><div><span>是否换标</span><strong>{{ selectedDetail?.needRelabel ? '是' : '否' }}</strong></div><div><span>换标状态</span><el-tag :type="transferRelabelStatusTagType(transferRelabelStatus(selectedDetail || {}).code)" effect="light">{{ transferRelabelStatus(selectedDetail || {}).label }}</el-tag></div><div><span>换标单号</span><strong>{{ selectedDetail?.relabelNo || '—' }}</strong></div><div><span>创建时间</span><strong>{{ formatDateTime(selectedDetail?.createdAt) }}</strong></div></div><div v-else-if="props.service === 'disposal'" class="warehouse-detail-grid"><div><span>废弃申请单号</span><strong>{{ selectedDetail?.discardNo || '—' }}</strong></div><div><span>废弃出库单号</span><strong>{{ selectedDetail?.outboundNo || '—' }}</strong></div><div><span>进仓编号</span><strong>{{ selectedDetail?.inboundCodes || '—' }}</strong></div><div><span>操作仓</span><strong>{{ selectedDetail?.overseasWarehouseName || '—' }}</strong></div><div><span>SKU 汇总</span><strong>{{ [...new Set((selectedDetail?.items || []).map(item => item.skuCode).filter(Boolean))].join('、') || '未记录' }}</strong></div><div><span>申请箱数</span><strong>{{ selectedDetail?.requestedPackages ?? '未记录' }}</strong></div><div><span>预估费用</span><strong>{{ selectedDetail?.currency || 'USD' }} {{ selectedDetail?.totalFee ?? '未记录' }}</strong></div><div><span>状态</span><strong>{{ statusLabel(selectedDetail?.status || '') }}</strong></div><div><span>废弃完成时间</span><strong>{{ formatDateTime(selectedDetail?.customerConfirmedAt) }}</strong></div><div><span>创建时间</span><strong>{{ formatDateTime(selectedDetail?.createdAt) }}</strong></div></div><div v-else 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>
|
||
<TransferPickupProgressPanel v-if="!detailError && props.service === 'transfer' && selectedDetail?.deliveryMethod === 'WAREHOUSE_PICKUP'" :transfer-no="String(selectedDetail.transferNo || '')" />
|
||
<div v-if="props.service === 'disposal'" class="warehouse-card warehouse-form-section">
|
||
<p class="warehouse-completion-remark">废弃备注:{{ selectedDetail?.remark || '—' }}</p><h3>本次废弃箱子</h3><div class="warehouse-table-scroll"><table class="warehouse-edit-table"><thead><tr><th>入仓码</th><th>SKU</th><th>品名</th><th>申请数量</th><th>所选箱唛</th><th>废弃托盘</th><th>实际废弃箱唛</th></tr></thead><tbody><tr v-for="(item, index) in selectedDetail?.items || []" :key="index"><td>{{ item.inboundCode || '—' }}</td><td>{{ item.skuCode || '—' }}</td><td>{{ item.productName || '—' }}</td><td>{{ item.discardPackages ?? '—' }} {{ item.quantityUnit || '单位未记录' }}<br>实际:{{ item.actualDiscardPackages == null ? '未记录' : `${item.actualDiscardPackages} ${item.quantityUnit || '单位未记录'}` }}</td><td>{{ item.cartonMarks?.join('、') || '未关联箱唛' }}</td><td>{{ item.palletNo || '—' }}</td><td>{{ item.actualCartonMarks?.join('、') || '未记录' }}</td></tr></tbody></table></div>
|
||
<template v-if="selectedDetail?.photos?.length"><h3>废弃现场照片</h3><div class="warehouse-photo-grid"><div v-for="photo in selectedDetail.photos" :key="photo.fileId" class="warehouse-photo-item"><el-image v-if="discardPhotoUrls[photo.fileId]" :src="discardPhotoUrls[photo.fileId]" :preview-src-list="discardPhotoPreviewUrls" :initial-index="discardPhotoPreviewUrls.indexOf(discardPhotoUrls[photo.fileId])" fit="cover" preview-teleported /><div v-else-if="discardPhotoLoading[photo.fileId]" class="warehouse-photo-placeholder">照片加载中…</div><button v-else class="warehouse-photo-placeholder warehouse-photo-retry" type="button" @click="loadDiscardPhoto(photo.fileId)">{{ discardPhotoErrors[photo.fileId] ? '加载失败,点击重试' : '加载照片' }}</button><small>{{ formatDateTime(photo.submittedAt) }}</small></div></div></template>
|
||
</div>
|
||
<div v-if="props.service === 'transfer' && selectedDetail?.status !== 'DRAFT'" class="warehouse-card warehouse-form-section">
|
||
<div class="warehouse-section-heading warehouse-transfer-receipt-heading"><h3>预报箱唛({{ selectedDetail?.packageCount || 0 }} 箱)</h3><div class="warehouse-transfer-receipt-actions"><el-button v-if="canIncreaseForecast" type="primary" size="small" @click="openIncreaseForecast"><Plus :size="14" />增加预报箱数</el-button><el-button type="primary" size="small" @click="printTransferCartons(String(selectedDetail?.transferNo || ''))"><Printer :size="14" />打印预报箱唛</el-button><el-button size="small" @click="showForecastCartons = true">查看预报箱唛</el-button></div></div>
|
||
<el-dialog v-model="showForecastCartons" title="预报箱唛明细" width="900px" align-center destroy-on-close append-to-body><el-table :data="selectedDetail?.cartons || []" size="small" border><el-table-column prop="cartonMark" label="仓库唛头" min-width="300" /><el-table-column label="预报SKU" min-width="240"><template #default="{ row }">{{ selectedDetail?.items?.find(item => String(item.id) === String(row.transferItemId))?.skuCode || '—' }}</template></el-table-column><el-table-column label="清点状态" width="130"><template #default="{ row }">{{ row.status === 'COUNTED' ? '已清点' : row.status === 'NOT_RECEIVED' ? '未收到' : '待清点' }}</template></el-table-column></el-table><template #footer><el-button @click="showForecastCartons = false">关闭</el-button></template></el-dialog>
|
||
<el-dialog v-model="increaseForecastOpen" title="增加预报箱数" width="560px" align-center destroy-on-close append-to-body :close-on-click-modal="!increasingForecast" :show-close="!increasingForecast">
|
||
<el-form label-width="110px"><el-form-item label="新增箱数"><el-input-number v-model="increaseForecastForm.additionalCount" placeholder="" :disabled="increasingForecast" :min="1" :max="Math.max(1, 5000 - Number(forecastSnapshot?.packageCount || 0))" :precision="0" /></el-form-item></el-form>
|
||
<template #footer><el-button :disabled="increasingForecast" @click="increaseForecastOpen = false">取消</el-button><el-button type="primary" :loading="increasingForecast" @click="saveIncreaseForecast">确认增加</el-button></template>
|
||
</el-dialog>
|
||
</div>
|
||
<div v-if="props.service === 'transfer'" class="warehouse-card warehouse-form-section warehouse-transfer-cargo-card">
|
||
<div class="warehouse-section-heading"><h3>预报与实际签收</h3></div>
|
||
<div class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-transfer-comparison-table"><colgroup><col style="width:500px" /><col style="width:140px" /><col style="width:130px" /><col style="width:100px" /><col style="width:130px" /><col style="width:130px" /></colgroup><thead><tr><th>商品 SKU</th><th>中文品名</th><th>预报数量(件)</th><th>预计箱数</th><th>实际签收件数</th><th>实际签收箱数</th></tr></thead><tbody><tr v-for="(item, index) in selectedDetail?.items || []" :key="String(item.id || index)"><td>{{ item.skuCode || '—' }}</td><td>{{ item.productNameZh || '—' }}</td><td>{{ item.quantity ?? '未记录' }}</td><td>{{ item.packageCount ?? '—' }}</td><td>{{ receivedItemQuantity(item.id) }}</td><td>{{ receivedItemCartons(item.id).length }}</td></tr><tr v-if="!selectedDetail?.items?.length"><td colspan="6"><div class="warehouse-empty">暂无货物明细</div></td></tr></tbody></table></div>
|
||
<details class="warehouse-transfer-pallet-group" open>
|
||
<summary class="warehouse-section-heading warehouse-transfer-pallet-heading warehouse-transfer-pallet-summary"><h3>托盘实测数据</h3><span class="warehouse-transfer-pallet-count">{{ selectedDetail?.pallets?.length || 0 }} 托</span><span class="warehouse-transfer-pallet-toggle"><span class="when-collapsed">展开托盘</span><span class="when-expanded">收起托盘</span><ChevronDown :size="14" /></span></summary>
|
||
<div v-if="selectedDetail?.pallets?.length" class="warehouse-transfer-pallet-list">
|
||
<article v-for="pallet in selectedDetail.pallets" :key="String(pallet.id || pallet.sequenceNo)" class="warehouse-transfer-pallet-card">
|
||
<header class="warehouse-transfer-pallet-header">
|
||
<strong><span>托盘号</span> {{ pallet.palletNo || `第${pallet.sequenceNo || '—'}托` }}</strong>
|
||
<dl class="warehouse-transfer-pallet-metrics">
|
||
<div><dt>箱数</dt><dd>{{ palletCartons(pallet).length }} 箱</dd></div><div><dt>件数</dt><dd>{{ palletQuantity(pallet) }} 件</dd></div><div><dt>SKU 种数</dt><dd>{{ palletSkuRows(pallet).length }} 种</dd></div><div><dt>毛重(kg)</dt><dd>{{ pallet.grossWeightKg ?? '—' }}</dd></div><div><dt>体积(m³)</dt><dd>{{ pallet.volumeCbm ?? '—' }}</dd></div>
|
||
</dl>
|
||
</header>
|
||
<div class="warehouse-transfer-pallet-skus"><span>SKU 汇总</span><div><div v-for="item in palletSkuRows(pallet)" :key="item.sku" class="warehouse-transfer-pallet-sku"><strong>{{ item.sku }}</strong><span>{{ item.quantity }} 件</span></div><span v-if="!palletSkuRows(pallet).length">暂无已清点 SKU</span></div></div>
|
||
<div class="warehouse-transfer-pallet-meta"><span>库位:{{ pallet.warehouseLocationCode || '—' }}</span><span>完成时间:{{ formatDateTime(pallet.completedAt) }}</span></div>
|
||
</article>
|
||
</div>
|
||
<div v-else class="warehouse-empty">暂无托盘数据</div>
|
||
</details>
|
||
<div v-if="selectedDetail?.measurementPhotos?.length" class="warehouse-evidence warehouse-measurement-evidence"><span>现场照片</span><div v-if="selectedDetail?.measurementPhotos?.length" class="warehouse-photo-grid warehouse-measurement-photo-grid"><div v-for="(photo, index) in selectedDetail.measurementPhotos" :key="photo.fileId" class="warehouse-photo-item warehouse-measurement-photo-item" :title="photo.fileName || `测量照片 ${index + 1}`"><el-image v-if="measurementPhotoUrls[photo.fileId]" :src="measurementPhotoUrls[photo.fileId]" :preview-src-list="measurementPhotoPreviewUrls" :initial-index="measurementPhotoPreviewUrls.indexOf(measurementPhotoUrls[photo.fileId])" :alt="photo.fileName || `测量照片 ${index + 1}`" fit="cover" preview-teleported /><div v-else-if="measurementPhotoLoading[photo.fileId]" class="warehouse-photo-placeholder">照片 {{ index + 1 }} 加载中…</div><button v-else class="warehouse-photo-placeholder warehouse-photo-retry" type="button" @click="loadMeasurementPhoto(photo.fileId)">{{ measurementPhotoErrors[photo.fileId] ? `照片 ${index + 1} 加载失败,点击重试` : `加载照片 ${index + 1}` }}</button></div></div></div>
|
||
</div>
|
||
<div v-if="props.service === 'transfer'" class="warehouse-card warehouse-form-section warehouse-transfer-received-card">
|
||
<div class="warehouse-section-heading warehouse-transfer-receipt-heading"><h3>签收箱号明细({{ countedTransferCartons().length }} 箱)</h3><div class="warehouse-transfer-receipt-actions"><el-button type="primary" size="small" :disabled="!countedTransferCartons().length" @click="showTransferCartons = true">查看箱号明细</el-button></div></div>
|
||
<div v-if="!countedTransferCartons().length" class="warehouse-empty">暂无签收箱号</div>
|
||
<el-dialog v-model="showTransferCartons" title="签收箱号明细" width="1120px" align-center destroy-on-close append-to-body>
|
||
<div class="warehouse-cartons-dialog-body">
|
||
<el-collapse>
|
||
<el-collapse-item v-for="carton in countedTransferCartons()" :key="carton.cartonMark" :name="carton.cartonMark">
|
||
<template #title><strong>{{ carton.cartonMark }}</strong><span class="warehouse-carton-summary">已确认 · {{ transferCartonLines(carton).length }} 种 SKU · {{ carton.quantity ?? '—' }} 件</span></template>
|
||
<div class="warehouse-carton-measurements">所属托盘:{{ selectedDetail?.pallets?.find((pallet) => carton.transferPalletId != null && String(pallet.id) === String(carton.transferPalletId))?.palletNo || '散箱' }} · 清点时间:{{ formatDateTime(carton.countedAt) }}</div>
|
||
<div class="warehouse-carton-measurements">长 {{ carton.length ?? '—' }} cm · 宽 {{ carton.width ?? '—' }} cm · 高 {{ carton.height ?? '—' }} cm · 重量 {{ carton.weight ?? '—' }} kg · 体积 {{ [carton.length, carton.width, carton.height].every(value => value != null) ? (Number(carton.length) * Number(carton.width) * Number(carton.height) / 1000000).toFixed(6) : '—' }} m³</div>
|
||
<el-table :data="transferCartonLines(carton)" size="small" border>
|
||
<el-table-column prop="skuCode" label="SKU" min-width="220" />
|
||
<el-table-column prop="productNameZh" label="中文品名" min-width="180" />
|
||
<el-table-column prop="quantity" label="实际签收数量" width="130" />
|
||
</el-table>
|
||
<p v-if="carton.discrepancyReason" class="warehouse-carton-measurements">差异说明:{{ carton.discrepancyReason }}</p>
|
||
</el-collapse-item>
|
||
</el-collapse>
|
||
</div>
|
||
<template #footer><el-button @click="showTransferCartons = false">关闭</el-button></template>
|
||
</el-dialog>
|
||
</div>
|
||
<div v-if="props.service === 'transfer' && selectedDetail?.needRelabel" class="warehouse-card warehouse-form-section">
|
||
<h3>换标箱级去向</h3>
|
||
<div class="warehouse-table-scroll"><table><thead><tr><th>换标类型</th><th>仓库唛头</th><th>旧 SKU</th><th>新 SKU</th><th>新 FBA 箱唛</th><th>实际 FBA 目的仓</th><th>数量</th><th>备注</th></tr></thead><tbody><tr v-for="(binding, index) in selectedDetail?.relabelBindings || []" :key="`${binding.customerCartonMark}-${index}`"><td>{{ binding.operationType === 'CARTON_AND_SKU' ? 'SKU 和箱唛都换' : binding.operationType === 'SKU_LABEL' ? '只换 SKU' : '只换箱唛' }}</td><td>{{ binding.customerCartonMark || '—' }}</td><td>{{ binding.originalSku || '—' }}</td><td>{{ binding.newSku || '—' }}</td><td>{{ binding.fbaCartonMark || '—' }}</td><td>{{ binding.operationType === 'SKU_LABEL' ? '沿用原箱去向' : warehouseDisplay(binding.destinationWarehouseCode) }}</td><td>{{ binding.quantity ?? '—' }}</td><td>{{ binding.remark || '—' }}</td></tr><tr v-if="!selectedDetail?.relabelBindings?.length"><td colspan="8"><div class="warehouse-empty">暂无换标绑定明细</div></td></tr></tbody></table></div>
|
||
</div>
|
||
<div v-if="props.service === 'labeling' && selectedRelabelDetail" class="warehouse-card warehouse-form-section">
|
||
<div class="warehouse-section-heading"><h3>换标完成与出库</h3></div>
|
||
<div class="warehouse-detail-grid warehouse-fulfillment-grid">
|
||
<div><span>派送方式</span><strong>{{ deliveryMethodLabel(selectedRelabelDetail?.application.deliveryMethod || '') || '无需出库' }}</strong></div>
|
||
<div><span>履约状态</span><el-tag :type="relabelFulfillmentTagType(selectedRelabelDetail?.application.fulfillmentStatus)" effect="light">{{ relabelFulfillmentLabel(selectedRelabelDetail?.application.fulfillmentStatus) }}</el-tag></div>
|
||
<div><span>客户确认时间</span><strong>{{ formatDateTime(selectedRelabelDetail?.application.customerConfirmedAt) }}</strong></div>
|
||
<div v-if="selectedRelabelDetail"><RelabelDocumentsPanel inline :relabel-no="selectedRelabelDetail.application.relabelNo" :detail="selectedRelabelDetail" /></div>
|
||
</div>
|
||
<div v-if="selectedRelabelDetail?.completionEvidence?.photoFileIds?.length" class="warehouse-evidence"><span>仓库完工照片</span><div v-if="selectedRelabelDetail?.completionEvidence?.photoFileIds?.length" class="warehouse-photo-grid"><div v-for="(fileId, index) in selectedRelabelDetail.completionEvidence.photoFileIds" :key="fileId" class="warehouse-photo-item"><el-image v-if="completionPhotoUrls[fileId]" :src="completionPhotoUrls[fileId]" :preview-src-list="completionPhotoPreviewUrls" :initial-index="completionPhotoPreviewUrls.indexOf(completionPhotoUrls[fileId])" fit="cover" preview-teleported /><div v-else-if="completionPhotoLoading[fileId]" class="warehouse-photo-placeholder">照片 {{ index + 1 }} 加载中…</div><button v-else class="warehouse-photo-placeholder warehouse-photo-retry" type="button" @click="loadCompletionPhoto(fileId)">{{ completionPhotoErrors[fileId] ? `照片 ${index + 1} 加载失败,点击重试` : `加载照片 ${index + 1}` }}</button></div></div></div>
|
||
<p v-if="selectedRelabelDetail?.completionEvidence?.completionRemark" class="warehouse-completion-remark">完工备注:{{ selectedRelabelDetail.completionEvidence.completionRemark }}</p>
|
||
</div>
|
||
<div v-if="props.service === 'transfer' && selectedDetail?.relabelNo" class="warehouse-card warehouse-form-section"><RelabelDocumentsPanel :relabel-no="selectedDetail.relabelNo" /></div>
|
||
<div v-if="props.service === 'labeling'" class="warehouse-card warehouse-form-section">
|
||
<div class="warehouse-section-heading"><h3>贴标明细资料</h3><input ref="relabelFileInput" class="warehouse-hidden-input" type="file" accept="application/pdf,.pdf" @change="uploadSelectedRelabelFile"></div>
|
||
|
||
<div class="warehouse-table-scroll"><table class="warehouse-relabel-detail-table"><thead><tr><th>换标模式</th><th>仓库唛头</th><th>SKU</th><th>明细文件</th><th>操作</th></tr></thead><tbody>
|
||
<tr v-for="item in selectedRelabelDetail?.items || []" :key="item.id"><td>{{ item.operationType === 'CARTON_AND_SKU' ? '箱唛 + SKU 换标' : ['SKU_LABEL', 'RETURN_BULK_SKU_LABEL'].includes(item.operationType || '') ? 'SKU 换标' : '箱唛换标' }}</td><td>{{ item.cartonMark || '—' }}</td><td>{{ relabelSkuCodes(item).join('、') || '—' }}</td><td>{{ item.relabelFileName || '未上传' }}</td><td><el-button size="small" @click="chooseRelabelFile('ITEM', String(item.id))"><Upload :size="14" />上传明细文件</el-button></td></tr>
|
||
<tr v-if="!selectedRelabelDetail?.items?.length"><td colspan="5"><div class="warehouse-empty">暂无贴标明细</div></td></tr>
|
||
</tbody></table></div>
|
||
</div>
|
||
<div v-if="props.service === 'labeling'" class="warehouse-card warehouse-form-section">
|
||
<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 换标' : ['SKU_LABEL', 'RETURN_BULK_SKU_LABEL'].includes(item.operationType || '') ? 'SKU 换标' : '箱唛换标' }}</td><td><strong>{{ item.cartonMark || '—' }}</strong></td><td><strong>{{ item.fbaCartonMark || '—' }}</strong></td><td>{{ warehouseDisplay(item.destinationFbaWarehouseCode) }}</td><td><span v-if="item.totalPackageCount == null" class="warehouse-missing-value">—</span><template v-else>{{ item.totalPackageCount }} {{ String(item.operationType) === 'RETURN_BULK_SKU_LABEL' ? '件' : '箱任务' }}</template></td><td>{{ relabelSkuCodes(item).join('、') || '—' }}</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>
|
||
</tbody></table></div>
|
||
</div>
|
||
</template>
|
||
</section>
|
||
<CompletionConfirmationDialog v-model="completionConfirmOpen" :kind="completionConfirmKind" :document-no="completionConfirmNo" @confirmed="refreshAfterCompletion" />
|
||
</template>
|
||
|
||
<style scoped>
|
||
.warehouse-service-page .warehouse-missing-value { color: #a8abb2; font-weight: 400; }
|
||
.warehouse-table-card :deep(.related-business-number-cell .cell) { white-space:normal; overflow:visible; text-overflow:clip; overflow-wrap:anywhere; padding-top:6px; padding-bottom:6px; }
|
||
.labeling-type-tags { display:inline-flex; flex-wrap:wrap; gap:4px; }
|
||
.labeling-query.query-toolbar--grid { display:grid; grid-template-columns:repeat(4,minmax(0,320px)); gap:14px 18px; align-items:end; }
|
||
.labeling-query :deep(.query-toolbar__filters) { display:contents; }
|
||
.labeling-query :deep(.query-toolbar__field) { display:flex; flex-direction:column; align-items:stretch; gap:6px; }
|
||
.labeling-query :deep(.query-toolbar__field:nth-child(-n+3)) { grid-row:1; }
|
||
.labeling-query :deep(.query-toolbar__field:nth-child(n+4)) { grid-row:2; }
|
||
.labeling-query :deep(.query-toolbar__label) { line-height:18px; }
|
||
.labeling-query :deep(.query-toolbar__actions) { grid-column:4; grid-row:1; margin:0; padding:0; border:0; align-self:end; }
|
||
.transfer-query.query-toolbar--grid { display:grid; grid-template-columns:repeat(5,minmax(0,280px)); gap:14px 18px; align-items:end; }
|
||
.transfer-query :deep(.query-toolbar__filters) { display:contents; }
|
||
.transfer-query :deep(.query-toolbar__field) { display:flex; flex-direction:column; align-items:stretch; gap:6px; }
|
||
.transfer-query :deep(.query-toolbar__field:nth-child(-n+5)) { grid-row:1; }
|
||
.transfer-query :deep(.query-toolbar__field:nth-child(n+6)) { grid-row:2; }
|
||
.transfer-query :deep(.query-toolbar__label) { line-height:18px; }
|
||
.transfer-query :deep(.query-toolbar__actions) { grid-column:5; grid-row:2; margin:0; padding:0; border:0; align-self:end; }
|
||
.disposal-query :deep(.query-toolbar__field:nth-child(5)) { grid-column:1; grid-row:2; }
|
||
.disposal-query :deep(.query-toolbar__actions) { grid-column:2; }
|
||
@media(max-width:1200px) {
|
||
.labeling-query.query-toolbar--grid { grid-template-columns:repeat(2,minmax(0,320px)); }
|
||
.labeling-query :deep(.query-toolbar__field:nth-child(n)) { grid-row:auto; }
|
||
.labeling-query :deep(.query-toolbar__actions) { grid-column:auto; grid-row:auto; }
|
||
.transfer-query.query-toolbar--grid { grid-template-columns:repeat(2,minmax(0,320px)); }
|
||
.transfer-query :deep(.query-toolbar__field:nth-child(n)) { grid-row:auto; }
|
||
.disposal-query :deep(.query-toolbar__field:nth-child(5)) { grid-column:auto; }
|
||
.transfer-query :deep(.query-toolbar__actions) { grid-column:auto; grid-row:auto; }
|
||
}
|
||
.warehouse-order-info-heading { display: flex; align-items: center; justify-content: space-between; max-width: 1296px; margin-bottom: 16px; border-bottom: 1px solid #edf0f4; padding-bottom: 10px; }
|
||
.warehouse-form-section .warehouse-order-info-heading h3 { margin: 0; padding: 0; border: 0; }
|
||
.warehouse-order-info-summary { display: flex; flex-wrap: wrap; gap: 10px 24px; color: #64748b; font-size: 12px; }
|
||
.warehouse-service-page--return-relabel .warehouse-transfer-cargo-card .warehouse-labeling-mode {
|
||
width: fit-content;
|
||
max-width: 1296px;
|
||
box-sizing: border-box;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 8px;
|
||
background: #f8fafc;
|
||
padding: 8px 12px;
|
||
gap: 14px;
|
||
margin: 20px 0 22px;
|
||
}
|
||
.warehouse-service-page--return-relabel .warehouse-transfer-cargo-card .warehouse-labeling-mode > span {
|
||
color: #475569;
|
||
font-size: 12px;
|
||
}
|
||
.warehouse-service-page--return-relabel .warehouse-transfer-cargo-card .warehouse-table-scroll th {
|
||
background: #f0f5fc;
|
||
color: #385579;
|
||
border-bottom: 1px solid #dce6f2;
|
||
height: 38px;
|
||
font-weight: 600;
|
||
}
|
||
.warehouse-service-page--return-relabel .warehouse-labeling-mode :deep(.el-radio-button) {
|
||
--el-color-primary: #2563eb;
|
||
--el-radio-button-checked-bg-color: #2563eb;
|
||
--el-radio-button-checked-border-color: #2563eb;
|
||
}
|
||
.warehouse-service-page--return-relabel .warehouse-labeling-mode :deep(.el-radio-button:not(.is-disabled) .el-radio-button__inner) {
|
||
border-color: #cbdaf0;
|
||
}
|
||
.warehouse-service-page--return-relabel .warehouse-labeling-mode :deep(.el-radio-group) { gap: 6px; }
|
||
.warehouse-service-page--return-relabel .warehouse-labeling-mode :deep(.el-radio-button .el-radio-button__inner) {
|
||
padding: 8px 12px;
|
||
border: 1px solid #dce3ec;
|
||
border-radius: 5px;
|
||
box-shadow: none;
|
||
font-size: 12px;
|
||
}
|
||
.warehouse-service-page--return-relabel .warehouse-labeling-mode :deep(.el-radio-button.is-disabled .el-radio-button__inner) {
|
||
color: #a3adbb;
|
||
background: #f1f4f8;
|
||
border-color: #e5eaf0;
|
||
}
|
||
.warehouse-service-page--return-relabel .warehouse-transfer-cargo-card .warehouse-table-scroll td { padding: 10px 12px; }
|
||
.warehouse-service-page--return-relabel .warehouse-transfer-cargo-card .warehouse-table-scroll h3 {
|
||
font-size: 14px;
|
||
margin-bottom: 12px;
|
||
padding-bottom: 10px;
|
||
}
|
||
.warehouse-service-page--return-relabel .warehouse-transfer-cargo-card .warehouse-sku-label-table :deep(.el-input__wrapper) {
|
||
min-height: 30px;
|
||
box-shadow: 0 0 0 1px #dce3ec inset;
|
||
border-radius: 5px;
|
||
}
|
||
.warehouse-service-page--return-relabel .warehouse-transfer-cargo-card .warehouse-sku-label-table :deep(.el-input-number) { width: 120px; }
|
||
.warehouse-service-page--return-relabel .warehouse-transfer-cargo-card .warehouse-tab-actions { margin-bottom: 10px; }
|
||
.warehouse-service-page{display:grid;gap:12px;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:0;border:1px solid var(--client-border);border-radius:var(--client-radius);background:#fff;box-shadow:none}.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-list-notice{padding:8px 12px;border-bottom:1px solid var(--client-border);background:#fff8e8;color:#8a6116;font-size:12px}.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:72px;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)}}
|
||
.warehouse-service-page--detail{display:block;overflow:hidden;border:1px solid var(--client-border);border-radius:var(--client-radius);background:#fff}.warehouse-detail-header{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:14px;min-height:64px;padding:10px 16px;border-bottom:1px solid var(--client-border)}.warehouse-detail-back{display:inline-flex;align-items:center;gap:5px;padding:5px 0;border:0;background:transparent;color:var(--el-color-primary);cursor:pointer}.warehouse-detail-title{display:flex;min-width:0;flex-direction:column;gap:3px}.warehouse-detail-title strong{overflow:hidden;color:#1f2d45;font-size:18px;text-overflow:ellipsis;white-space:nowrap}.warehouse-detail-title span{color:#7c899c;font-size:12px}.warehouse-detail-actions{display:flex;align-items:center;gap:8px}.warehouse-service-page--detail>.warehouse-card{border:0;border-top:1px solid var(--client-border);border-radius:0}.warehouse-service-page--detail>.warehouse-detail-header+.warehouse-card{border-top:0}.warehouse-service-page--detail>.warehouse-form-section{padding:0}.warehouse-service-page--detail .warehouse-form-section h3{min-height:40px;margin:0;padding:11px 16px 9px;border-bottom:1px solid #edf1f5;font-size:14px}.warehouse-service-page--detail .warehouse-detail-grid{grid-template-columns:repeat(4,minmax(0,1fr));gap:0;margin:0}.warehouse-service-page--detail .warehouse-detail-grid>div{min-height:48px;flex-direction:row;align-items:center;gap:10px;padding:8px 14px;border-right:1px solid #edf1f5;border-bottom:1px solid #edf1f5}.warehouse-service-page--detail .warehouse-detail-grid span{flex:0 0 90px}.warehouse-service-page--detail .warehouse-detail-grid strong{min-width:0;overflow-wrap:anywhere}.warehouse-service-page--detail .warehouse-fulfillment-grid{margin-bottom:0}.warehouse-service-page--detail .warehouse-evidence{margin:12px 14px}.warehouse-service-page--detail .warehouse-completion-remark,.warehouse-service-page--detail .warehouse-hint{margin:10px 14px}.warehouse-service-page--detail .warehouse-table-scroll table{margin:0}.warehouse-service-page--detail .warehouse-empty{height:64px}@media(max-width:1100px){.warehouse-service-page--detail .warehouse-detail-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:720px){.warehouse-detail-header{grid-template-columns:1fr auto}.warehouse-detail-back{grid-column:1/-1}.warehouse-detail-actions{justify-content:flex-end}}
|
||
.warehouse-detail-error{display:flex;min-height:220px;align-items:center;justify-content:center;gap:12px;color:#b42318}
|
||
.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}
|
||
.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}
|
||
.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-upload-summary{display:flex;flex-wrap:wrap;gap:8px 24px;margin:-4px 0 14px;padding:9px 12px;border:1px solid #dce7f5;border-radius:4px;background:#f8fbff;color:#596579;font-size:12px}
|
||
.warehouse-upload-summary>span{display:inline-flex;align-items:center;gap:6px;overflow-wrap:anywhere}
|
||
.warehouse-file-groups{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:16px;margin:0 0 18px}.warehouse-file-groups--single{grid-template-columns:minmax(0,1fr)}.warehouse-file-group{min-width:0;overflow:hidden;border:1px solid #dde5f0;border-radius:8px;background:#fbfcfe;box-shadow:0 1px 2px rgba(31,54,86,.04)}.warehouse-file-group--primary{border-color:#cbdcf2;background:linear-gradient(180deg,#f7faff 0,#fbfdff 100%)}.warehouse-file-group__heading{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;padding:14px 16px;border-bottom:1px solid #e7edf5;background:rgba(255,255,255,.68)}.warehouse-file-group__heading strong{display:flex;align-items:center;gap:8px;color:#2d405c;font-size:14px}.warehouse-file-group__heading strong>span{display:inline-grid;width:22px;height:22px;border-radius:50%;background:#3478c8;color:#fff;font-size:12px;place-items:center}.warehouse-file-group__heading em{flex-shrink:0;padding:4px 8px;border-radius:10px;background:#edf4fd;color:#4774aa;font-size:11px;font-style:normal}.warehouse-file-options{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:stretch;padding:14px}.warehouse-file-option{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:10px 12px;align-items:center;min-width:0;padding:12px;border:1px dashed #ced8e6;border-radius:6px;background:#fff}.warehouse-file-option.is-selected{border-style:solid;border-color:#79a8df;background:#f5f9ff}.warehouse-file-option__name strong{color:#34445a;font-size:13px}.warehouse-file-option__status{display:flex;grid-column:1/-1;align-items:center;justify-content:space-between;gap:8px;min-width:0;padding:7px 8px;border-radius:4px;background:#eaf3ff;color:#376da9;font-size:11px}.warehouse-file-option__status>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.warehouse-file-option__status.is-empty{background:#f4f6f8;color:#9aa4b2}.warehouse-file-choice-or{display:grid;color:#9ba6b5;font-size:11px;place-items:center}.warehouse-file-choice-or.is-both{color:#3478c8;font-weight:700}.warehouse-template-list{display:flex;flex-direction:column;padding:7px 14px}.warehouse-template-list>div{display:grid;grid-template-columns:minmax(150px,1fr) auto auto;align-items:center;gap:8px;padding:9px 0;border-bottom:1px solid #edf1f6}.warehouse-template-list>div:last-child{border-bottom:0}.warehouse-template-list strong{color:#3f4e63;font-size:12px}.warehouse-transfer-template-group{margin:0 0 14px}@media(max-width:1180px){.warehouse-file-groups{grid-template-columns:minmax(0,1fr)}}@media(max-width:720px){.warehouse-file-options{grid-template-columns:minmax(0,1fr)}.warehouse-file-choice-or{height:24px}.warehouse-template-list>div{grid-template-columns:minmax(0,1fr) auto}.warehouse-template-list>div>span{grid-column:1/-1}}
|
||
.warehouse-file-clear{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:24px;height:24px;border:0;border-radius:4px;background:transparent;color:var(--muted);cursor:pointer}
|
||
.warehouse-file-clear:hover:not(:disabled){color:#dc2626;background:#fee2e2}
|
||
.warehouse-file-clear:disabled{opacity:.5;cursor:not-allowed}
|
||
.warehouse-unmaintained{margin-left:8px;color:#e6a23c;font-size:12px;font-style:normal}
|
||
.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}
|
||
.warehouse-relabel-detail-table{min-width:1420px}.warehouse-relabel-detail-table td strong{color:#253f67}
|
||
.warehouse-file-groups--compact {
|
||
width: 100%;
|
||
max-width: 1296px;
|
||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||
gap: 16px;
|
||
align-items: stretch;
|
||
}
|
||
.warehouse-file-groups--compact .warehouse-file-group__heading {
|
||
padding: 10px 12px;
|
||
align-items: center;
|
||
}
|
||
.warehouse-file-groups--compact .warehouse-file-options {
|
||
padding: 10px;
|
||
grid-template-columns: minmax(0, 1fr) 20px minmax(0, 1fr);
|
||
}
|
||
.warehouse-file-groups--compact .warehouse-file-option {
|
||
padding: 8px;
|
||
gap: 8px;
|
||
}
|
||
.warehouse-file-groups--compact .warehouse-file-option__name strong { font-size: 12px; }
|
||
.warehouse-file-groups--compact .warehouse-file-option :deep(.el-button) {
|
||
padding: 6px 8px;
|
||
font-size: 12px;
|
||
}
|
||
.warehouse-file-groups--compact .warehouse-template-list { padding: 4px 12px; }
|
||
.warehouse-file-groups--compact .warehouse-template-list > div {
|
||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||
padding: 7px 0;
|
||
}
|
||
@media (max-width: 1180px) {
|
||
.warehouse-file-groups--compact { grid-template-columns: minmax(0, 1fr); }
|
||
}
|
||
@media (max-width: 720px) {
|
||
.warehouse-file-groups--compact .warehouse-file-options { grid-template-columns: minmax(0, 1fr); }
|
||
.warehouse-file-groups--compact .warehouse-template-list > div { grid-template-columns: minmax(0, 1fr) auto; }
|
||
}
|
||
.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:flex-start;gap:16px;padding:14px;border:1px solid #e7ebf1;border-radius:4px;background:#f8fafc}.warehouse-evidence>span{padding-top:6px;color:#71809a;font-size:12px;white-space:nowrap}.warehouse-evidence>strong{padding-top:6px;color:#8a95a5;font-size:12px;font-weight:400}.warehouse-photo-grid{display:grid;grid-template-columns:repeat(4,140px);gap:12px}.warehouse-photo-item{width:140px;height:105px;overflow:hidden;border:1px solid #dce3ed;border-radius:5px;background:#fff}.warehouse-photo-item :deep(.el-image){display:block;width:100%;height:100%;cursor:zoom-in}.warehouse-photo-placeholder{display:flex;width:100%;height:100%;align-items:center;justify-content:center;padding:10px;border:0;background:#f4f7fb;color:#8792a2;font-size:12px;text-align:center}.warehouse-photo-retry{cursor:pointer}.warehouse-completion-remark{margin:10px 0 0;color:#596579;font-size:12px}
|
||
.warehouse-measurement-evidence{margin-top:16px}
|
||
.warehouse-measurement-photo-grid{flex:1;grid-template-columns:repeat(auto-fill,minmax(240px,1fr))}
|
||
.warehouse-measurement-photo-item{width:100%;height:120px}
|
||
.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}
|
||
.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-file-groups{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(360px,.85fr);align-items:stretch;gap:14px;margin-bottom:14px}.warehouse-transfer-total-files,.warehouse-transfer-template-group{height:100%;margin:0}.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,var(--transfer-left,43fr)) 16px minmax(0,var(--transfer-right,57fr));column-gap:0;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:3;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 minmax(0,1fr);gap:10px}.warehouse-transfer-form .warehouse-choice-field>span{font-size:12px}.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 :deep(.el-radio-button__inner){padding:6px 9px}.warehouse-transfer-form .warehouse-template-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;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}.warehouse-step-heading{align-items:flex-start}.warehouse-step-heading h3{display:flex;align-items:center;gap:9px}.warehouse-step-number{display:inline-grid;flex:0 0 auto;width:24px;height:24px;border-radius:50%;background:#3478c8;color:#fff;font-size:12px;place-items:center}.warehouse-heading-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px}.warehouse-transfer-template-group{margin:0 0 14px;padding:12px 14px;border:1px solid #dde5f0;border-radius:7px;background:#fbfcfe}.warehouse-binding-heading{margin-top:14px;padding:12px 14px;border:1px solid #dde5f0;border-radius:7px;background:#f8faff}.warehouse-binding-heading strong{display:flex;align-items:center;gap:8px;color:#34445c;font-size:13px}
|
||
.warehouse-transfer-form .warehouse-choice-field--relabel{grid-template-columns:150px auto minmax(240px,1fr)}.warehouse-choice-field--relabel :deep(.el-radio-group){display:flex;flex-wrap:nowrap}.warehouse-choice-field--relabel>small{min-width:0;margin:0;line-height:1.5}
|
||
@media(max-width:1250px){.warehouse-transfer-form .warehouse-template-actions{justify-content:flex-start}.warehouse-transfer-form .warehouse-labeling-heading{align-items:flex-start;flex-direction:column}}
|
||
@media(max-width:1180px){.warehouse-transfer-file-groups{grid-template-columns:minmax(0,1fr)}}
|
||
.warehouse-transfer-form .pickup-address-field{grid-column:span 1}
|
||
.warehouse-transfer-main-card .warehouse-form-grid{grid-template-columns:repeat(2,minmax(280px,640px))}.warehouse-transfer-cargo-card .warehouse-section-heading,.warehouse-transfer-cargo-card .warehouse-table-scroll{width:100%;max-width:1296px}
|
||
.warehouse-service-page--detail .warehouse-transfer-cargo-card .warehouse-section-heading,.warehouse-service-page--detail .warehouse-transfer-cargo-card .warehouse-table-scroll{max-width:none}
|
||
.warehouse-service-page--detail .warehouse-transfer-overview-grid{grid-template-columns:repeat(4,minmax(180px,1fr));gap:6px 28px;padding:10px 16px 12px}.warehouse-service-page--detail .warehouse-transfer-overview-grid>div{min-height:26px;padding:2px 0;border:0}.warehouse-service-page--detail .warehouse-transfer-overview-grid span{flex:0 0 auto;min-width:76px}.warehouse-service-page--detail .warehouse-transfer-overview-grid :deep(.el-tag){height:24px}
|
||
.warehouse-service-page--detail>.warehouse-detail-header+.warehouse-card .warehouse-detail-grid{grid-template-columns:repeat(4,minmax(180px,1fr));gap:6px 28px;padding:10px 16px 12px}.warehouse-service-page--detail>.warehouse-detail-header+.warehouse-card .warehouse-detail-grid>div{min-height:26px;padding:2px 0;border:0}.warehouse-service-page--detail>.warehouse-detail-header+.warehouse-card .warehouse-detail-grid span{flex:0 0 auto;min-width:76px}
|
||
.warehouse-service-page--detail .warehouse-transfer-overview-grid>.warehouse-overview-wide{grid-column:span 2}
|
||
.warehouse-transfer-received-card{margin-top:14px}.warehouse-cartons-dialog-body{max-height:65vh;overflow:auto}.warehouse-carton-summary{margin-left:16px;color:var(--muted);font-size:12px}.warehouse-carton-measurements{margin:10px 0;color:var(--muted);font-size:13px;line-height:1.8}
|
||
@media(max-width:1100px){.warehouse-service-page--detail .warehouse-transfer-overview-grid{grid-template-columns:repeat(2,minmax(180px,1fr))}}@media(max-width:720px){.warehouse-service-page--detail .warehouse-transfer-overview-grid{grid-template-columns:1fr;gap:4px;padding:10px 14px}}
|
||
/* Keep the count and controls visible together on narrow list pages. */
|
||
.warehouse-pagination { justify-content: flex-end; }
|
||
.warehouse-pagination-total { color: #526176; font-size: 14px; }
|
||
.warehouse-pagination :deep(.el-pagination) { display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; margin: 0; }
|
||
.warehouse-page-size-select { width: 112px; margin-left: 8px; }
|
||
.warehouse-page-size-select :deep(.el-input__inner), .warehouse-page-size-select :deep(.el-select__selected-item) { color: #526176 !important; opacity: 1 !important; -webkit-text-fill-color: #526176; }
|
||
.warehouse-list-actions { display: flex; align-items: center; gap: 8px; white-space: nowrap; }
|
||
.warehouse-disposal-field { max-width: 520px; }
|
||
.warehouse-service-page--disposal-form .warehouse-form-grid { grid-template-columns:repeat(2,minmax(0,1fr)); }
|
||
.warehouse-service-page--disposal-form .warehouse-disposal-field,
|
||
.warehouse-service-page--disposal-form .warehouse-order-info-heading,
|
||
.warehouse-service-page--disposal-form .warehouse-section-heading,
|
||
.warehouse-service-page--disposal-form .warehouse-table-scroll { width:100%; max-width:none; }
|
||
@media (max-width: 720px) {
|
||
.warehouse-pagination :deep(.el-pagination) { width: 100%; justify-content: flex-end; }
|
||
.warehouse-transfer-main-card .warehouse-form-grid { grid-template-columns: minmax(0, 1fr); }
|
||
.warehouse-transfer-cargo-card .warehouse-section-heading, .warehouse-transfer-cargo-card .warehouse-table-scroll { max-width: none; }
|
||
.warehouse-transfer-form .warehouse-choice-field--relabel { grid-template-columns: 1fr; align-items: start; }
|
||
}
|
||
|
||
.warehouse-transfer-form .warehouse-form-grid { grid-template-columns:repeat(2,minmax(0,1fr)); }
|
||
.warehouse-transfer-form .warehouse-form-section { min-width:0; }
|
||
.warehouse-transfer-form .warehouse-section-heading,
|
||
.warehouse-transfer-form .warehouse-table-scroll,
|
||
.warehouse-transfer-form .warehouse-order-info-heading,
|
||
.warehouse-transfer-form .warehouse-file-groups--compact { width:100%; max-width:none; }
|
||
.warehouse-transfer-form--relabel .warehouse-transfer-divider { grid-column:2; grid-row:1 / span 2; }
|
||
</style>
|
||
|
||
<style scoped>
|
||
.warehouse-discard-boxes { margin-bottom: 24px; }
|
||
.warehouse-discard-box-toolbar { display: flex; align-items: center; gap: 20px; margin-bottom: 16px; }
|
||
.warehouse-discard-box-toolbar .el-input { max-width: 340px; }
|
||
|
||
.warehouse-service-page--detail .warehouse-table-scroll .warehouse-transfer-comparison-table{width:1130px;min-width:1130px;table-layout:fixed}
|
||
.warehouse-transfer-comparison-table td{white-space:normal;overflow-wrap:anywhere}
|
||
.warehouse-service-page--detail .warehouse-transfer-comparison-table th{height:36px;padding:0 10px}
|
||
.warehouse-service-page--detail .warehouse-transfer-comparison-table td{height:38px;padding:7px 10px}
|
||
.warehouse-transfer-comparison-table th:nth-child(n+3),.warehouse-transfer-comparison-table td:nth-child(n+3){text-align:center}
|
||
.warehouse-transfer-comparison-table td:first-child{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.warehouse-service-page--detail .warehouse-transfer-cargo-card .warehouse-transfer-pallet-heading{margin-top:12px}
|
||
.warehouse-transfer-pallet-list{display:grid;grid-template-columns:minmax(0,1fr);gap:12px;padding:0 14px;margin-bottom:12px;align-items:start}
|
||
.warehouse-transfer-pallet-card{padding:14px;border:1px solid var(--client-border);border-radius:10px;background:#fff;min-width:0}
|
||
.warehouse-transfer-pallet-header{display:flex;align-items:center;flex-wrap:wrap;gap:10px 20px}
|
||
.warehouse-transfer-pallet-header>strong{font-size:13px;color:var(--navy);overflow-wrap:anywhere;line-height:1.6}
|
||
.warehouse-transfer-pallet-header strong>span{display:inline-block;margin-right:8px;padding:2px 7px;border-radius:4px;background:#eef1ff;color:var(--primary);font-size:12px;font-weight:500}
|
||
.warehouse-transfer-pallet-metrics{display:flex;flex-wrap:wrap;gap:8px;margin:0}
|
||
.warehouse-transfer-pallet-metrics>div{display:flex;align-items:baseline;gap:8px;padding:6px 10px;background:#f5f7fc;border:1px solid #e9edf5;border-radius:6px}
|
||
.warehouse-transfer-pallet-metrics dt{font-size:12px;color:var(--muted)}
|
||
.warehouse-transfer-pallet-metrics dd{margin:0;font-size:13px;font-weight:600;color:var(--navy)}
|
||
.warehouse-transfer-pallet-summary{cursor:pointer;list-style:none;border-radius:4px}
|
||
.warehouse-transfer-pallet-summary::-webkit-details-marker{display:none}
|
||
.warehouse-transfer-pallet-summary:focus-visible{outline:2px solid var(--primary);outline-offset:4px}
|
||
.warehouse-transfer-pallet-toggle{display:flex;align-items:center;gap:8px;flex-shrink:0;margin-left:6px}
|
||
.warehouse-transfer-pallet-count{font-size:12px;color:var(--muted)}
|
||
.warehouse-service-page--detail.warehouse-service-page--transfer>.warehouse-detail-header{grid-template-columns:auto minmax(0,max-content) auto;justify-content:start;column-gap:14px}
|
||
.warehouse-service-page--detail .warehouse-transfer-pallet-heading h3{flex:0 0 auto}
|
||
.warehouse-service-page--detail .warehouse-transfer-pallet-heading .warehouse-transfer-pallet-count{flex:0 0 auto}
|
||
.warehouse-service-page--detail .warehouse-transfer-cargo-card .warehouse-transfer-pallet-heading{display:flex;align-items:center;justify-content:flex-start;gap:10px}
|
||
.warehouse-transfer-pallet-toggle{color:var(--primary);font-size:12px;gap:4px}
|
||
.warehouse-transfer-pallet-group .when-expanded{display:none}
|
||
.warehouse-transfer-pallet-group[open] .when-expanded{display:inline}
|
||
.warehouse-transfer-pallet-group[open] .when-collapsed{display:none}
|
||
.warehouse-transfer-pallet-group[open] .warehouse-transfer-pallet-toggle svg{transform:rotate(180deg)}
|
||
.warehouse-transfer-pallet-meta{display:flex;flex-wrap:wrap;gap:4px 20px;margin-top:8px;color:var(--muted);font-size:12px;line-height:1.7;overflow-wrap:anywhere}
|
||
.warehouse-transfer-pallet-skus{display:grid;grid-template-columns:64px minmax(0,1fr);align-items:start;gap:10px;margin-top:10px;padding-top:10px;border-top:1px solid var(--client-border)}
|
||
.warehouse-transfer-pallet-skus>span{font-size:12px;color:var(--muted);padding-top:5px}
|
||
.warehouse-transfer-pallet-skus>div{display:flex;flex-wrap:wrap;gap:6px;min-width:0}
|
||
.warehouse-transfer-pallet-sku{display:inline-flex;align-items:center;gap:8px;max-width:100%;box-sizing:border-box;padding:4px 8px;background:#f5f7ff;border:1px solid #e5e9f8;border-radius:5px;font-size:12px;line-height:1.6}
|
||
.warehouse-transfer-pallet-sku strong{font-weight:500;overflow-wrap:anywhere;min-width:0;color:var(--navy)}
|
||
.warehouse-transfer-pallet-sku>span{flex-shrink:0;padding:0 7px;border:1px solid #e0e6ff;border-radius:4px;background:#eef1ff;color:var(--primary);font-weight:600;white-space:nowrap}
|
||
.warehouse-service-page--detail.warehouse-service-page--transfer .warehouse-section-heading{box-sizing:border-box;min-height:44px;justify-content:flex-start;flex-wrap:wrap;gap:10px 16px;padding:8px 16px;border-bottom:1px solid #edf1f5}
|
||
.warehouse-service-page--detail.warehouse-service-page--transfer .warehouse-section-heading h3{flex:0 0 auto;min-height:0;margin:0;padding:0;border:0;line-height:24px}
|
||
.warehouse-service-page--detail.warehouse-service-page--transfer .warehouse-transfer-cargo-card>.warehouse-table-scroll{box-sizing:border-box;padding:0 16px}
|
||
.warehouse-service-page--detail.warehouse-service-page--transfer .warehouse-transfer-comparison-table th:first-child,.warehouse-service-page--detail.warehouse-service-page--transfer .warehouse-transfer-comparison-table td:first-child{padding-left:0}
|
||
.warehouse-service-page--detail.warehouse-service-page--transfer .warehouse-transfer-pallet-list{padding:0 16px}
|
||
.warehouse-service-page--detail.warehouse-service-page--transfer .warehouse-measurement-evidence{margin:12px 16px}
|
||
.warehouse-transfer-receipt-actions{display:flex;flex-wrap:wrap;gap:8px}.warehouse-transfer-receipt-actions :deep(.el-button+.el-button){margin-left:0}
|
||
.warehouse-service-page--detail.warehouse-service-page--transfer>.warehouse-card .warehouse-transfer-overview-grid{width:100%;max-width:1440px;box-sizing:border-box;grid-template-columns:repeat(4,minmax(0,1fr));column-gap:18px;row-gap:4px;padding:8px 16px 10px}
|
||
.warehouse-service-page--detail.warehouse-service-page--transfer>.warehouse-card .warehouse-transfer-overview-grid>div{min-height:24px;gap:8px;padding:1px 0}
|
||
.warehouse-service-page--detail.warehouse-service-page--transfer>.warehouse-card .warehouse-transfer-overview-grid>div>span{flex:0 0 84px;min-width:0}
|
||
</style>
|
||
|
||
<style scoped>
|
||
.warehouse-create {
|
||
--el-color-primary: #2f7bff;
|
||
--el-text-color-regular: #1f2937;
|
||
--el-text-color-placeholder: #b0b7c3;
|
||
--client-border: #e3e8f0;
|
||
width: 100%; min-width: 0; gap: 0; background: #f4f7fc; color: #1f2937;
|
||
font: 14px/1.5 "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif;
|
||
}
|
||
.warehouse-create .warehouse-page-heading { margin-bottom: 12px; }
|
||
.warehouse-create .warehouse-page-heading h2 { margin: 0; color: #1f2937; font-size: 26px; font-weight: 700; }
|
||
.warehouse-create .warehouse-form-section { min-width: 0; margin-bottom: 12px; padding: 0; border: 0; background: transparent; box-shadow: none; }
|
||
.warehouse-create .warehouse-order-info-heading,
|
||
.warehouse-create .warehouse-labeling-heading { width: 100%; max-width: none; margin: 0 0 10px; padding: 0; border: 0; }
|
||
.warehouse-create .warehouse-form-section h3 { display: flex; align-items: center; gap: 10px; margin: 0; padding: 0; border: 0; color: #1f2937; font-size: 18px; font-weight: 700; }
|
||
.warehouse-create .warehouse-form-section h3::before { content: ''; width: 9px; height: 9px; flex: 0 0 9px; border-radius: 50%; background: #2f7bff; }
|
||
.warehouse-create .warehouse-labeling-heading h3::before { background: #0ea5a4; }
|
||
.warehouse-create .warehouse-form-grid,
|
||
.warehouse-create .warehouse-labeling-body,
|
||
.warehouse-create .warehouse-order-info-summary { width: 100%; max-width: none; min-width: 0; box-sizing: border-box; padding: 16px; border: 1px solid #e3e8f0; border-radius: 10px; background: #fff; }
|
||
.warehouse-create .warehouse-form-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px 20px; }
|
||
.warehouse-create :deep(.el-form-item) { min-width: 0; margin: 0; }
|
||
.warehouse-create :deep(label.el-form-item__label) { display: flex; flex-direction: row; gap: 0; height: auto; margin: 0 0 6px; padding: 0; color: #374151; font-size: 14px; font-weight: 600; line-height: 20px; }
|
||
.warehouse-create :deep(.el-form-item.is-required .el-form-item__label::before) { color: #e5484d; }
|
||
.warehouse-create :deep(.el-form-item__content) { min-width: 0; line-height: 20px; }
|
||
/* Validation shares the label line so showing and clearing it cannot move the next row. */
|
||
.warehouse-create :deep(.el-form-item__error) { position: absolute; top: auto; bottom: calc(100% + 6px); right: 0; left: auto; width: auto; max-width: calc(100% - 170px); padding: 0; font-size: 12px; line-height: 20px; text-align: right; }
|
||
.warehouse-create .warehouse-form-grid :deep(.el-input),
|
||
.warehouse-create .warehouse-form-grid :deep(.el-select),
|
||
.warehouse-create .warehouse-form-grid :deep(.el-date-editor) { width: 100%; }
|
||
.warehouse-create .warehouse-form-grid :deep(.el-input__wrapper),
|
||
.warehouse-create .warehouse-form-grid :deep(.el-select__wrapper) { min-height: 44px; box-sizing: border-box; padding: 10px 14px; border: 1px solid #d5dce8; border-radius: 6px; background: #fff; box-shadow: none; }
|
||
.warehouse-create .warehouse-form-grid :deep(.el-input__inner) { height: 22px; line-height: 22px; color: #1f2937; font-size: 14px; }
|
||
.warehouse-create .warehouse-form-grid :deep(.el-textarea__inner) { min-height: 110px !important; padding: 11px 14px; border: 1px solid #d5dce8; border-radius: 6px; box-shadow: none; font-size: 14px; }
|
||
.warehouse-create .warehouse-form-grid :deep(.el-checkbox) { height: 44px; }
|
||
.warehouse-create .warehouse-form-grid :deep(.el-checkbox__label) { font-size: 14px; }
|
||
.warehouse-create .warehouse-form-grid :deep(.el-input__wrapper.is-focus),
|
||
.warehouse-create .warehouse-form-grid :deep(.el-select__wrapper.is-focused),
|
||
.warehouse-create .warehouse-form-grid :deep(.el-textarea__inner:focus) { border-color: #2f7bff; }
|
||
.warehouse-create .warehouse-form-grid :deep(.is-error .el-input__wrapper),
|
||
.warehouse-create .warehouse-form-grid :deep(.is-error .el-select__wrapper) { border-color: #e5484d; box-shadow: none; }
|
||
.warehouse-create :deep(input::placeholder),
|
||
.warehouse-create :deep(textarea::placeholder),
|
||
.warehouse-create :deep(.el-select__placeholder.is-transparent) { color: #b0b7c3; opacity: 1; }
|
||
.warehouse-create .warehouse-native-file { width: 100%; min-height: 44px; box-sizing: border-box; padding: 8px 10px; border-color: #d5dce8; border-radius: 6px; }
|
||
.warehouse-service-page.warehouse-create .warehouse-transfer-cargo-card .warehouse-section-heading,
|
||
.warehouse-service-page.warehouse-create .warehouse-transfer-cargo-card .warehouse-table-scroll,
|
||
.warehouse-service-page.warehouse-create .warehouse-file-groups--compact { width: 100%; max-width: none; }
|
||
.warehouse-service-page.warehouse-create .warehouse-file-groups { gap: 12px; margin-bottom: 12px; }
|
||
.warehouse-service-page.warehouse-create .warehouse-file-group { border: 1px solid #e3e8f0; border-radius: 6px; background: #fff; box-shadow: none; }
|
||
.warehouse-service-page.warehouse-create .warehouse-file-group__heading { padding: 10px 12px; background: #fff; }
|
||
.warehouse-service-page.warehouse-create .warehouse-file-group__options { padding: 10px 12px; }
|
||
.warehouse-service-page.warehouse-create .warehouse-labeling-mode { width: auto; max-width: none; gap: 12px; margin: 12px 0; padding: 10px 12px; border-color: #e3e8f0; border-radius: 6px; background: #fff; }
|
||
.warehouse-service-page.warehouse-create .warehouse-tab-actions { margin: 0 0 10px; }
|
||
.warehouse-service-page.warehouse-create :deep(.el-radio-button__inner),
|
||
.warehouse-service-page.warehouse-create .warehouse-labeling-body :deep(.el-input__wrapper),
|
||
.warehouse-service-page.warehouse-create .warehouse-labeling-body :deep(.el-textarea__inner) { box-shadow: none; }
|
||
.warehouse-service-page.warehouse-create .warehouse-labeling-body :deep(.el-input__wrapper),
|
||
.warehouse-service-page.warehouse-create .warehouse-labeling-body :deep(.el-textarea__inner) { border: 1px solid #d5dce8; border-radius: 6px; }
|
||
.warehouse-create .warehouse-submit-bar { justify-content: flex-end; gap: 12px; padding: 0; }
|
||
.warehouse-create :deep(.el-button) { box-shadow: none; }
|
||
.warehouse-create .warehouse-page-heading :deep(.el-button),
|
||
.warehouse-create .warehouse-submit-bar :deep(.el-button) { min-height: 44px; margin-left: 0; padding: 10px 18px; border: 1px solid #d5dce8; border-radius: 6px; background: #fff; color: #1d5fe0; font-size: 14px; font-weight: 600; }
|
||
.warehouse-create .warehouse-submit-bar :deep(.el-button--primary) { padding: 12px 28px; border-color: #2f7bff; background: #2f7bff; color: #fff; }
|
||
.warehouse-create .warehouse-submit-bar :deep(.el-button.is-disabled) { opacity: .55; }
|
||
:global(.labeling-create-popper.el-popper) { border: 1px solid #e3e8f0; border-radius: 6px; box-shadow: none; }
|
||
</style>
|
||
|
||
<style scoped>
|
||
.warehouse-transfer-create .warehouse-transfer-form { gap: 12px; }
|
||
.warehouse-transfer-create .warehouse-transfer-form--relabel { column-gap: 0; }
|
||
.warehouse-transfer-create .warehouse-form-section { margin-bottom: 0; padding: 0; }
|
||
.warehouse-transfer-create .warehouse-transfer-form .warehouse-form-grid { --el-component-size: 44px; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px 20px; }
|
||
/* Preserve the existing resizable two-pane workflow when relabeling is selected. */
|
||
.warehouse-transfer-create .warehouse-transfer-form--relabel .warehouse-form-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||
.warehouse-transfer-create .warehouse-transfer-create-body { min-width: 0; padding: 16px; border: 1px solid #e3e8f0; border-radius: 10px; background: #fff; }
|
||
.warehouse-transfer-create .warehouse-transfer-create-body > .warehouse-section-heading h3 { display: none; }
|
||
.warehouse-transfer-create .warehouse-transfer-create-body > .warehouse-section-heading { justify-content: flex-end; }
|
||
.warehouse-transfer-create .warehouse-transfer-create-body .warehouse-form-grid { padding: 0; border: 0; border-radius: 0; }
|
||
.warehouse-transfer-create .warehouse-transfer-relabel { overflow: visible; }
|
||
.warehouse-transfer-create .warehouse-step-number { display: none; }
|
||
.warehouse-transfer-create .warehouse-transfer-form .warehouse-form-section h3 { margin: 0; padding: 0; font-size: 18px; }
|
||
.warehouse-transfer-create .warehouse-transfer-cargo-card > .warehouse-order-info-heading h3::before { background: #0ea5a4; }
|
||
.warehouse-transfer-create .warehouse-transfer-relabel > .warehouse-section-heading h3::before { background: #0ea5a4; }
|
||
.warehouse-transfer-create .warehouse-transfer-relabel > .warehouse-section-heading { margin-bottom: 10px; }
|
||
.warehouse-transfer-create .warehouse-transfer-create-body section h3 { font-size: 14px; }
|
||
.warehouse-transfer-create .warehouse-transfer-create-body section h3::before { display: none; }
|
||
.warehouse-transfer-create .warehouse-receiving-card { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 20px; padding: 12px; border: 1px solid #e3e8f0; border-radius: 6px; background: #f4f7fc; }
|
||
.warehouse-transfer-create .warehouse-receiving-card > .el-form-item { display: block; }
|
||
.warehouse-transfer-create .warehouse-receiving-card > div > span { font-size: 14px; }
|
||
.warehouse-transfer-create :deep(.el-form-item__error) { max-width: calc(100% - 140px); }
|
||
.warehouse-transfer-create .warehouse-form-grid :deep(.el-radio-button__inner) { padding: 11px 14px; font-size: 14px; line-height: 20px; box-shadow: none; }
|
||
.warehouse-transfer-create .warehouse-transfer-create-body .warehouse-transfer-file-groups { gap: 12px; margin-bottom: 12px; }
|
||
.warehouse-transfer-create .warehouse-transfer-create-body .warehouse-file-group { border: 1px solid #e3e8f0; border-radius: 6px; background: #fff; box-shadow: none; }
|
||
.warehouse-transfer-create .warehouse-transfer-create-body .warehouse-labeling-mode { margin: 12px 0; padding: 10px 12px; border: 1px solid #e3e8f0; border-radius: 6px; background: #fff; }
|
||
.warehouse-transfer-create .warehouse-transfer-create-body .warehouse-edit-table { width: 100%; font-size: 13px; }
|
||
.warehouse-transfer-create .warehouse-transfer-create-body .warehouse-edit-table th { height: 40px; font-size: 13px; }
|
||
.warehouse-transfer-create .warehouse-transfer-create-body .warehouse-edit-table td { padding: 8px; }
|
||
.warehouse-transfer-create .warehouse-table-scroll :deep(.el-input__wrapper),
|
||
.warehouse-transfer-create .warehouse-table-scroll :deep(.el-textarea__inner) { border: 1px solid #d5dce8; border-radius: 6px; box-shadow: none; }
|
||
.warehouse-transfer-create .warehouse-table-scroll :deep(.el-input__wrapper.is-focus),
|
||
.warehouse-transfer-create .warehouse-table-scroll :deep(.el-textarea__inner:focus) { border-color: #2f7bff; }
|
||
.warehouse-transfer-create .warehouse-transfer-form .warehouse-submit-bar { justify-content: flex-end; gap: 12px; padding: 0; }
|
||
.warehouse-transfer-create .warehouse-transfer-form .warehouse-submit-bar :deep(.el-button) { min-height: 44px; padding: 10px 18px; }
|
||
.warehouse-transfer-create .warehouse-transfer-form .warehouse-submit-bar :deep(.el-button--primary) { padding: 12px 28px; }
|
||
.warehouse-transfer-create .warehouse-form-grid :deep(.el-date-editor.el-input) { height: 44px; }
|
||
.warehouse-transfer-create .warehouse-transfer-form--relabel .warehouse-transfer-choice { grid-column: 1 / -1; }
|
||
.warehouse-transfer-create .warehouse-form-grid :deep(.el-input__wrapper.is-focus),
|
||
.warehouse-transfer-create .warehouse-form-grid :deep(.el-select__wrapper.is-focused),
|
||
.warehouse-transfer-create .warehouse-form-grid :deep(.el-textarea__inner:focus) { outline: 2px solid #2f7bff; outline-offset: 0; }
|
||
:global(.transfer-create-popper.el-popper) { border: 1px solid #e3e8f0; border-radius: 6px; box-shadow: none; }
|
||
</style>
|
||
|
||
<style scoped>
|
||
.warehouse-disposal-create .warehouse-form-grid { --el-component-size: 44px; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px 20px; }
|
||
.warehouse-disposal-create .warehouse-disposal-create-body { min-width: 0; padding: 16px; border: 1px solid #e3e8f0; border-radius: 10px; background: #fff; }
|
||
.warehouse-disposal-create .warehouse-transfer-cargo-card > .warehouse-order-info-heading h3::before { background: #0ea5a4; }
|
||
.warehouse-disposal-create .warehouse-disposal-create-body > .warehouse-section-heading { display: none; }
|
||
.warehouse-disposal-create .warehouse-discard-boxes > .warehouse-section-heading h3 { display: none; }
|
||
.warehouse-disposal-create .warehouse-discard-boxes > .warehouse-section-heading { justify-content: flex-end; margin: 0 0 12px; }
|
||
.warehouse-disposal-create .warehouse-discard-box-toolbar { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
|
||
.warehouse-disposal-create .warehouse-discard-box-toolbar :deep(.el-input) { width: calc((100% - 40px) / 3); }
|
||
.warehouse-disposal-create .warehouse-edit-table { width: 100%; font-size: 13px; }
|
||
.warehouse-disposal-create .warehouse-edit-table th { height: 40px; font-size: 13px; }
|
||
.warehouse-disposal-create .warehouse-edit-table td { padding: 8px 12px; }
|
||
.warehouse-disposal-create .warehouse-disposal-create-body :deep(.el-input__wrapper) { border: 1px solid #d5dce8; border-radius: 6px; box-shadow: none; }
|
||
.warehouse-disposal-create .warehouse-form-grid :deep(.el-input__wrapper.is-focus),
|
||
.warehouse-disposal-create .warehouse-form-grid :deep(.el-select__wrapper.is-focused),
|
||
.warehouse-disposal-create .warehouse-form-grid :deep(.el-textarea__inner:focus),
|
||
.warehouse-disposal-create .warehouse-disposal-create-body :deep(.el-input__wrapper.is-focus) { outline: 2px solid #2f7bff; outline-offset: 0; }
|
||
.warehouse-disposal-create .warehouse-hint { margin: 10px 0 0; color: #6b7280; font-size: 13px; line-height: 20px; }
|
||
.warehouse-disposal-create .warehouse-disposal-quantity { padding: 0; border: 0; }
|
||
.warehouse-disposal-create .warehouse-disposal-quantity :deep(.el-input-number) { width: 100%; }
|
||
.warehouse-disposal-create .warehouse-disposal-quantity :deep(.el-input-number .el-input__wrapper) { padding-right: 40px; }
|
||
.warehouse-disposal-create .warehouse-disposal-quantity > .warehouse-hint { align-self: end; margin-bottom: 12px; }
|
||
:global(.disposal-create-popper.el-popper) { border: 1px solid #e3e8f0; border-radius: 6px; box-shadow: none; }
|
||
</style>
|