贴标优化0923

This commit is contained in:
2026-09-23 11:25:35 +08:00
parent 25db3c1777
commit bc5c56d0e4
4 changed files with 260 additions and 39 deletions
@@ -5,8 +5,8 @@ export type RelabelDeliveryMethod = 'TRUCK' | 'EXPRESS' | 'TRUCK_PICKUP' | 'EXPR
export interface PageResult<T> { records: T[]; page: number; size: number; total: number; pages: number } export interface PageResult<T> { records: T[]; page: number; size: number; total: number; pages: number }
export interface LastlegOrderSummary { id: string; orderNo: string; inboundCode: string; orderType: string; packageCount: number; overseasWarehouseId?: string } export interface LastlegOrderSummary { id: string; orderNo: string; inboundCode: string; orderType: string; packageCount: number; overseasWarehouseId?: string }
export interface HistoricalOrderSummary { id: string | null; orderNo: string; sourceOrderNo?: string; outboundOrderNo?: string; relabelOrderNo?: string; orderType?: string; status?: string; packageCount?: number } export interface HistoricalOrderSummary { id: string | null; orderNo: string; sourceOrderNo?: string; outboundOrderNo?: string; relabelOrderNo?: string; orderType?: string; status?: string; packageCount?: number }
export interface TransferSummary { id: string; transferNo: string; inboundCode: string; lastlegOrderId: string; lastlegOrderNo: string; operatingWarehouseId: string; fbaWarehouseCode?: string; packageCount: number; deliveryMethod: string; needRelabel: boolean; relabelNo?: string; status: string; estimatedArrivalAt: string; estimatedShipAt?: string; actualShipAt?: string; createdAt: string } export interface TransferSummary { id: string; transferNo: string; inboundCode: string; lastlegOrderId: string; lastlegOrderNo: string; operatingWarehouseId: string; fbaWarehouseCode?: string; packageCount: number; deliveryMethod: string; needRelabel: boolean; relabelNo?: string; relabelStatus?: string; relabelFulfillmentStatus?: string; status: string; estimatedArrivalAt: string; estimatedShipAt?: string; actualShipAt?: string; createdAt: string }
export interface RelabelSummary { id: string; relabelNo: string; lastlegOrderId: string; lastlegOrderNo: string; customerOrderNo: string; overseasWarehouseId: string; status: string; itemCount: number; totalPackageCount: number; notifiedPackageCount: number; skuCodes: string; createdAt: string; totalFileId?: string; totalFileName?: string; totalFileUrl?: string; cartonTotalFileId?: string; cartonTotalFileName?: string; cartonTotalFileUrl?: string; skuTotalFileId?: string; skuTotalFileName?: string; skuTotalFileUrl?: string; deliveryMethod?: RelabelDeliveryMethod; printShippingLabel?: boolean; shippingLabelFileId?: string; shippingLabelFileName?: string; shippingLabelFileUrl?: string; fulfillmentStatus?: string; outboundOrderNo?: string; customerConfirmedAt?: string } export interface RelabelSummary { id: string; relabelNo: string; lastlegOrderId: string; lastlegOrderNo: string; customerOrderNo: string; overseasWarehouseId: string; status: string; itemCount: number; totalPackageCount: number; notifiedPackageCount: number; skuCodes: string; createdAt: string; totalFileId?: string; totalFileName?: string; totalFileUrl?: string; cartonTotalFileId?: string; cartonTotalFileName?: string; cartonTotalFileUrl?: string; skuTotalFileId?: string; skuTotalFileName?: string; skuTotalFileUrl?: string; deliveryMethod?: RelabelDeliveryMethod; printShippingLabel?: boolean; shippingLabelFileId?: string; shippingLabelFileName?: string; shippingLabelFileUrl?: string; fulfillmentStatus?: string; outboundOrderNo?: string; customerConfirmedAt?: string; reviewChannel?: 'TAIL_ORDER' | 'RELABEL_ORDER'; reviewRemark?: string }
export interface RelabelItemDetail { export interface RelabelItemDetail {
id: string; skuCode?: string; productNameZh: string; totalPackageCount: number; notifiedPackageCount: number id: string; skuCode?: string; productNameZh: string; totalPackageCount: number; notifiedPackageCount: number
shipmentNo: string; trackingCode?: string; newSkuCode?: string; labelRequirement?: string; remark?: string shipmentNo: string; trackingCode?: string; newSkuCode?: string; labelRequirement?: string; remark?: string
@@ -97,6 +97,9 @@ export function listRelabelInventoryCandidates(operationWarehouseId: string, inb
export function createRelabel(payload: RelabelCreate) { export function createRelabel(payload: RelabelCreate) {
return overseasApiRequest<RelabelDetail>({ method: 'POST', url: '/merchant/v1/last-leg/relabel-applications', data: payload }) return overseasApiRequest<RelabelDetail>({ method: 'POST', url: '/merchant/v1/last-leg/relabel-applications', data: payload })
} }
export function resubmitRelabel(relabelNo: string, payload: { labelDeadline: string; contactName: string; contactPhone: string; remark?: string; idempotencyKey: string }) {
return overseasApiRequest<RelabelDetail>({ method: 'PUT', url: `/merchant/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/resubmit`, data: payload })
}
export function uploadRelabelShippingLabel(relabelNo: string, file: File) { export function uploadRelabelShippingLabel(relabelNo: string, file: File) {
const data = new FormData(); data.append('file', file) const data = new FormData(); data.append('file', file)
return overseasApiRequest<RelabelDetail>({ method: 'POST', url: `/merchant/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/shipping-label`, data, headers: { 'Content-Type': 'multipart/form-data' } }) return overseasApiRequest<RelabelDetail>({ method: 'POST', url: `/merchant/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/shipping-label`, data, headers: { 'Content-Type': 'multipart/form-data' } })
@@ -8,7 +8,7 @@ import DataTable, { type DataTableColumn } from '@/components/common/DataTable.v
import type { TableAction } from '@/components/common/tableActions' import type { TableAction } from '@/components/common/tableActions'
import { useAuthStore } from '@/modules/auth/public' import { useAuthStore } from '@/modules/auth/public'
import { import {
createDiscard, createRelabel, createTransfer, getDiscard, getRelabel, getTransfer, createDiscard, createRelabel, createTransfer, getDiscard, getRelabel, getTransfer, resubmitRelabel,
getTransferCartonLabel, submitTransfer, updateTransfer, getTransferCartonLabel, submitTransfer, updateTransfer,
confirmRelabelCompletion, confirmTransferMeasurement, downloadRelabelCompletionPhoto, downloadTransferMeasurementPhoto, uploadRelabelShippingLabel, confirmRelabelCompletion, confirmTransferMeasurement, downloadRelabelCompletionPhoto, downloadTransferMeasurementPhoto, uploadRelabelShippingLabel,
uploadRelabelFile, uploadRelabelFile,
@@ -55,7 +55,7 @@ const canConfirmTransfer = computed(() => auth.hasPermission('merchant:overseas-
const canCancelDiscard = computed(() => auth.hasPermission('merchant:overseas-warehouse:discard:cancel')) const canCancelDiscard = computed(() => auth.hasPermission('merchant:overseas-warehouse:discard:cancel'))
const configs: Record<ServiceType, ServiceConfig> = { const configs: Record<ServiceType, ServiceConfig> = {
labeling: { title: '贴标申请', createTitle: '新增贴标申请', numberLabel: '贴标申请单号', emptyText: '暂无贴标申请', columns: ['操作', '贴标申请单号', '客户单号', '海外操作仓', '贴标要求', 'SKU 数', '总箱数', '状态', '创建时间'] }, labeling: { title: '贴标申请', createTitle: '新增贴标申请', numberLabel: '贴标申请单号', emptyText: '暂无贴标申请', columns: ['操作', '贴标申请单号', '客户单号', '海外操作仓', '贴标要求', 'SKU 数', '总箱数', '状态', '创建时间'] },
transfer: { title: '仓储转运单', createTitle: '新增仓储转运单', numberLabel: '转运单号', emptyText: '暂无仓储转运订单', columns: ['操作', '转运单号', '入库箱数', '海外仓', 'FBA 目的仓', '入仓方式', '是否换标', '状态', '创建时间'] }, transfer: { title: '仓储转运单', createTitle: '新增仓储转运单', numberLabel: '转运单号', emptyText: '暂无仓储转运订单', columns: ['操作', '转运单号', '箱数', '海外仓', 'FBA 目的仓', '入仓方式', '是否换标', '换标单号', '换标状态', '状态', '创建时间'] },
disposal: { title: '货物废弃', createTitle: '新增货物废弃申请', numberLabel: '废弃申请单号', emptyText: '暂无货物废弃申请', columns: ['操作', '废弃申请单号', '进仓编号', '操作仓', 'SKU 汇总', '废弃数量', '预估费用', '状态', '创建时间'] }, disposal: { title: '货物废弃', createTitle: '新增货物废弃申请', numberLabel: '废弃申请单号', emptyText: '暂无货物废弃申请', columns: ['操作', '废弃申请单号', '进仓编号', '操作仓', 'SKU 汇总', '废弃数量', '预估费用', '状态', '创建时间'] },
relabel: { title: '退货换标', createTitle: '新建退货换标单', numberLabel: '退货换标单号', emptyText: '暂无退货换标订单', columns: ['操作', '退货换标单号', '来源 FBA 仓', '目的 FBA 仓', 'SKU 汇总', '退件总数', '是否换标', '换标状态', '尾程状态', '创建时间'] }, relabel: { title: '退货换标', createTitle: '新建退货换标单', numberLabel: '退货换标单号', emptyText: '暂无退货换标订单', columns: ['操作', '退货换标单号', '来源 FBA 仓', '目的 FBA 仓', 'SKU 汇总', '退件总数', '是否换标', '换标状态', '尾程状态', '创建时间'] },
} }
@@ -70,7 +70,7 @@ const listQueryModel = computed<QueryModel>({
}), }),
}) })
const statusOptions = computed(() => props.service === 'labeling' const statusOptions = computed(() => props.service === 'labeling'
? [{ label: '待处理', value: 'PENDING_PROCESSING' }, { label: '处理中', value: 'PROCESSING' }, { label: '换标完成', value: 'COMPLETED' }] ? [{ label: '待审核', value: 'PENDING_REVIEW' }, { label: '待处理', value: 'PENDING_PROCESSING' }, { label: '处理中', value: 'PROCESSING' }, { label: '换标完成', value: 'COMPLETED' }, { label: '已驳回', value: 'REJECTED' }]
: props.service === 'disposal' : props.service === 'disposal'
? [{ label: '待审核', value: 'PENDING_REVIEW' }, { label: '待 PDA 废弃', value: 'APPROVED' }, { label: '待仓库确认完成', value: 'PDA_COMPLETED' }, { label: '已完成', value: 'COMPLETED' }, { label: '已拒绝', value: 'REJECTED' }, { label: '已取消', value: 'CANCELLED' }] ? [{ label: '待审核', value: 'PENDING_REVIEW' }, { label: '待 PDA 废弃', value: 'APPROVED' }, { label: '待仓库确认完成', value: 'PDA_COMPLETED' }, { label: '已完成', value: 'COMPLETED' }, { label: '已拒绝', value: 'REJECTED' }, { label: '已取消', value: 'CANCELLED' }]
: props.service === 'transfer' : props.service === 'transfer'
@@ -111,13 +111,14 @@ interface TransferPallet { id?: string | number; sequenceNo?: number; palletNo?:
interface TransferMeasurementPhoto { fileId: string | number; fileName?: string; contentType?: string; sizeBytes?: number; uploadedAt?: string } interface TransferMeasurementPhoto { fileId: string | number; fileName?: string; contentType?: string; sizeBytes?: number; uploadedAt?: string }
interface ServiceDetail extends Record<string, unknown> { interface ServiceDetail extends Record<string, unknown> {
transferNo?: string; status?: string; operatingWarehouseId?: string; fbaWarehouseCode?: string transferNo?: string; status?: string; operatingWarehouseId?: string; fbaWarehouseCode?: string
destinationType?: 'FBA' | 'PRIVATE'; destinationWarehouseCode?: string; destinationWarehouseName?: string destinationType?: 'NONE' | 'FBA' | 'PRIVATE'; destinationWarehouseCode?: string; destinationWarehouseName?: string
privateDestinationAddress?: string; privateDestinationPostalCode?: string; privateDestinationContactName?: string; privateDestinationContactPhone?: string privateDestinationAddress?: string; privateDestinationPostalCode?: string; privateDestinationContactName?: string; privateDestinationContactPhone?: string
deliveryAddress?: string; pickupContactName?: string; pickupContactPhone?: string deliveryAddress?: string; pickupContactName?: string; pickupContactPhone?: string
packageCount?: number; volumeCbm?: number; deliveryMethod?: string; estimatedArrivalAt?: string packageCount?: number; volumeCbm?: number; deliveryMethod?: string; estimatedArrivalAt?: string
estimatedShipAt?: string; actualShipAt?: string; fbaShipmentNo?: string; trackingNo?: string estimatedShipAt?: string; actualShipAt?: string; fbaShipmentNo?: string; trackingNo?: string
remark?: string; needRelabel?: boolean; labelDeadline?: string; relabelContactName?: string remark?: string; needRelabel?: boolean; labelDeadline?: string; relabelContactName?: string
relabelContactPhone?: string; relabelDeliveryMethod?: string relabelContactPhone?: string; relabelDeliveryMethod?: string; relabelNo?: string
relabelStatus?: string; relabelFulfillmentStatus?: string; createdAt?: string
items?: Array<{ skuCode?: string; productNameZh?: string; packageCount?: number; shippingMark?: string }> items?: Array<{ skuCode?: string; productNameZh?: string; packageCount?: number; shippingMark?: string }>
relabelBindings?: Array<{ operationType?: string; customerCartonMark?: string; originalSku?: string; newSku?: string; quantity?: number; fbaCartonMark?: string; destinationWarehouseCode?: string; remark?: 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 cartons?: TransferCarton[]; pallets?: TransferPallet[]; measurementPhotos?: TransferMeasurementPhoto[]; customerConfirmedAt?: string
@@ -158,7 +159,8 @@ const listQueryFields = computed<QueryField[]>(() => {
}) })
const listColumns = computed<DataTableColumn[]>(() => config.value.columns.map((label, index) => index === 0 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: 'actions', label, slotName: 'actions', width: props.service === 'transfer' ? 210 : 160, fixed: 'left' }
: { key: String(index - 1), prop: String(index - 1), label, minWidth: index === 1 ? 190 : 130 })) : { key: String(index - 1), prop: String(index - 1), label, minWidth: index === 1 || label === '换标单号' ? 190 : 130,
slotName: props.service === 'transfer' && label === '换标状态' ? 'transferRelabelStatus' : props.service === 'transfer' && label === '状态' ? 'transferStatus' : undefined }))
const discardCandidates = ref<import('../api/warehouseServices').DiscardCandidate[]>([]) const discardCandidates = ref<import('../api/warehouseServices').DiscardCandidate[]>([])
const selectedDiscardOrderId = ref('') const selectedDiscardOrderId = ref('')
const discardCandidatesLoading = ref(false) const discardCandidatesLoading = ref(false)
@@ -225,6 +227,13 @@ watch(() => form.relabel, (value) => {
if (!transferRelabelRows.value.length) addTransferRelabelRow() 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 (value !== 'carton' && !skuRelabelRows.value.length) addSkuRelabelRow() }) watch(labelingMode, (value) => { if (value !== 'carton' && !skuRelabelRows.value.length) addSkuRelabelRow() })
watch(() => form.destination, (value) => { watch(() => form.destination, (value) => {
if (props.service !== 'labeling') return if (props.service !== 'labeling') return
@@ -397,7 +406,10 @@ function printTransferDraftCartonMarks() {
printWindow.document.write(`<!doctype html><html><head><meta charset="utf-8"><title>仓储转运客户箱唛</title><style>@page{size:100mm 100mm;margin:8mm}body{margin:0;font-family:Arial,'Microsoft YaHei',sans-serif;color:#111}.label{height:84mm;display:flex;flex-direction:column;align-items:center;justify-content:center;page-break-after:always;border:1px solid #222;box-sizing:border-box;padding:8mm}.title{font-size:18px;font-weight:700}.mark{margin:11mm 0 6mm;font-size:26px;font-weight:700;letter-spacing:1px;word-break:break-all;text-align:center}.sku{font-size:17px;font-weight:650}.meta{font-size:12px;margin-top:4mm}</style></head><body>${sections}</body></html>`) printWindow.document.write(`<!doctype html><html><head><meta charset="utf-8"><title>仓储转运客户箱唛</title><style>@page{size:100mm 100mm;margin:8mm}body{margin:0;font-family:Arial,'Microsoft YaHei',sans-serif;color:#111}.label{height:84mm;display:flex;flex-direction:column;align-items:center;justify-content:center;page-break-after:always;border:1px solid #222;box-sizing:border-box;padding:8mm}.title{font-size:18px;font-weight:700}.mark{margin:11mm 0 6mm;font-size:26px;font-weight:700;letter-spacing:1px;word-break:break-all;text-align:center}.sku{font-size:17px;font-weight:650}.meta{font-size:12px;margin-top:4mm}</style></head><body>${sections}</body></html>`)
printWindow.document.close(); printWindow.focus(); printWindow.print() printWindow.document.close(); printWindow.focus(); printWindow.print()
} }
function addTransferRelabelRow() { transferRelabelRows.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() { function clearTransferRelabelRows() {
transferRelabelRows.value = [] transferRelabelRows.value = []
} }
@@ -728,9 +740,12 @@ function warehouseDisplay(value?: string) {
} }
function statusLabel(value: string) { function statusLabel(value: string) {
if (props.service === 'labeling') { if (props.service === 'labeling') {
if (value === 'PROCESSING') return '处理中' const relabelLabels: Record<string, string> = {
if (value === 'CUSTOMER_CONFIRM_PENDING') return '待客户确认' WAIT_APPROVAL: '待审核', PENDING_REVIEW: '待审核', PENDING_PROCESSING: '待仓库处理', PROCESSING: '处理中',
return ['READY_FOR_OUTBOUND', 'COMPLETED'].includes(value) ? '换标完成' : '待处理' CUSTOMER_CONFIRM_PENDING: '待客户确认', READY_FOR_OUTBOUND: '换标完成', COMPLETED: '已完成',
REJECTED: '已驳回', CANCELLED: '已取消',
}
return relabelLabels[value] || value || '—'
} }
if (props.service === 'transfer') { if (props.service === 'transfer') {
const transferLabels: Record<string, string> = { const transferLabels: Record<string, string> = {
@@ -755,6 +770,55 @@ function statusLabel(value: string) {
} }
return labels[value] || value || '—' return labels[value] || value || '—'
} }
type StatusTagType = 'primary' | 'success' | 'warning' | 'danger' | 'info'
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) { function deliveryMethodLabel(value: string) {
const labels: Record<string, string> = { const labels: Record<string, string> = {
TRUCK: '卡车派送', EXPRESS: '快递派送', TRUCK_PICKUP: '卡车自提', EXPRESS_PICKUP: '快递自提', TRUCK: '卡车派送', EXPRESS: '快递派送', TRUCK_PICKUP: '卡车自提', EXPRESS_PICKUP: '快递自提',
@@ -764,6 +828,11 @@ function deliveryMethodLabel(value: string) {
} }
return labels[value] || value || '—' 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) { function formatDateTime(value?: string | null) {
return value ? value.replace('T', ' ').slice(0, 19) : '—' return value ? value.replace('T', ' ').slice(0, 19) : '—'
} }
@@ -786,8 +855,9 @@ async function load() {
total.value = Number.isFinite(Number(result.total)) ? Number(result.total) : result.records.length total.value = Number.isFinite(Number(result.total)) ? Number(result.total) : result.records.length
submittedRows.value = result.records.map((item) => { submittedRows.value = result.records.map((item) => {
const operationWarehouse = warehouseDisplay(item.operatingWarehouseId) const operationWarehouse = warehouseDisplay(item.operatingWarehouseId)
const relabelStatus = transferRelabelStatus(item)
return props.service === 'transfer' return props.service === 'transfer'
? { ...row([item.transferNo, item.packageCount, operationWarehouse, item.fbaWarehouseCode, deliveryMethodLabel(item.deliveryMethod), item.needRelabel ? '是 · 待绑定换标单' : '否', statusLabel(item.status), formatDateTime(item.createdAt)]), __status: item.status } ? { ...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.createdAt)]) : row([item.transferNo, '—', item.fbaWarehouseCode, '—', item.packageCount, '是', item.relabelNo ? '已生成' : '待生成', statusLabel(item.status), formatDateTime(item.createdAt)])
}) })
} }
@@ -964,8 +1034,13 @@ async function submit(action: 'draft' | 'submit' = 'submit') {
const submitted = await submitTransfer(transferNo) const submitted = await submitTransfer(transferNo)
if (form.relabel) { if (form.relabel) {
const relabelNo = String(submitted.relabelNo || created.relabelNo || '') const relabelNo = String(submitted.relabelNo || created.relabelNo || '')
if (!relabelNo) {
ElMessage.success('转运单已提交审核;PDA 入库确认后将自动生成关联换标单')
await ElMessageBox.alert(`转运单 ${transferNo} 已提交。关联换标单会在 PDA 完成入库、客户确认实测库存后自动生成;届时请进入关联换标单详情上传贴标总文件。`, '转运单已提交', { confirmButtonText: '知道了' })
await openDetail({ '0': transferNo })
return
}
try { try {
if (!relabelNo) throw new Error('转运单已提交,但未返回关联换标单号')
if (cartonTotalFile.value) await uploadRelabelFile(relabelNo, 'CARTON_TOTAL', cartonTotalFile.value) if (cartonTotalFile.value) await uploadRelabelFile(relabelNo, 'CARTON_TOTAL', cartonTotalFile.value)
if (skuTotalFile.value) await uploadRelabelFile(relabelNo, 'SKU_TOTAL', skuTotalFile.value) if (skuTotalFile.value) await uploadRelabelFile(relabelNo, 'SKU_TOTAL', skuTotalFile.value)
} catch (error) { } catch (error) {
@@ -1003,8 +1078,8 @@ async function openDetail(value: Record<string, string>) {
} catch (error) { selectedDetail.value = null; selectedRelabelDetail.value = null; detailError.value = error instanceof Error ? error.message : '详情加载失败'; mode.value = 'detail'; ElMessage.error(detailError.value) } } 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() { async function loadDetailPage() {
await loadWarehouses()
await openDetail({ '0': detailNumber.value }) await openDetail({ '0': detailNumber.value })
await loadWarehouses()
} }
async function confirmTransferMeasurements() { async function confirmTransferMeasurements() {
const transferNo = String(selectedDetail.value?.transferNo || selectedDetail.value?.['0'] || '') const transferNo = String(selectedDetail.value?.transferNo || selectedDetail.value?.['0'] || '')
@@ -1145,6 +1220,34 @@ async function selectTransferWarehouse() {
ElMessage.error(error instanceof Error ? error.message : '中转仓联系信息加载失败') ElMessage.error(error instanceof Error ? error.message : '中转仓联系信息加载失败')
} finally { operationWarehouseDetailLoading.value = false } } finally { operationWarehouseDetailLoading.value = false }
} }
async function confirmSelectedRelabel() {
const relabelNo = selectedRelabelDetail.value?.application.relabelNo
if (!relabelNo) return
try {
await ElMessageBox.confirm('确认仓库上传的换标结果无误并继续出库流程吗?', '换标完成确认', { confirmButtonText: '确认并继续', cancelButtonText: '取消' })
await confirmRelabelCompletion(relabelNo)
ElMessage.success('已确认换标完成,订单已进入后续出库流程')
await openDetail({ '0': relabelNo })
} catch (error) { if (error !== 'cancel') ElMessage.error(error instanceof Error ? error.message : '确认失败') }
}
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: crypto.randomUUID(),
})
ElMessage.success('贴标单已重新提交审核')
} catch (error) {
if (error !== 'cancel' && error !== 'close') ElMessage.error(error instanceof Error ? error.message : '重新提交失败')
}
}
const transferInventoryOptions = computed(() => { const transferInventoryOptions = computed(() => {
const grouped = new Map<string, { value: string; label: string; packages: number }>() const grouped = new Map<string, { value: string; label: string; packages: number }>()
for (const item of transferInventoryCandidates.value) { for (const item of transferInventoryCandidates.value) {
@@ -1264,6 +1367,8 @@ function selectRelabelInventory(inboundOrderId: string) {
<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"> <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 #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 v-if="props.service === 'transfer' && submittedRows.some((item) => item.__status === 'WAIT_CUSTOMER_CONFIRMATION')" #before-table><div class="warehouse-list-notice">部分入库完成订单已有 PDA 实测数据,请在对应行确认测量数据。</div></template> <template v-if="props.service === 'transfer' && submittedRows.some((item) => item.__status === 'WAIT_CUSTOMER_CONFIRMATION')" #before-table><div class="warehouse-list-notice">部分入库完成订单已有 PDA 实测数据,请在对应行确认测量数据。</div></template>
<template #transferRelabelStatus="{ row }"><el-tag :type="transferRelabelStatusTagType(row.__relabelStatus)" effect="light">{{ row.__relabelStatusLabel }}</el-tag></template>
<template #transferStatus="{ row }"><el-tag :type="transferStatusTagType(row.__status)" effect="light">{{ row.__statusLabel }}</el-tag></template>
</DataTable> </DataTable>
</div> </div>
</template> </template>
@@ -1304,7 +1409,7 @@ function selectRelabelInventory(inboundOrderId: string) {
<label v-if="props.service === 'relabel'" class="warehouse-span-2"><span><i>*</i>详细派送地址</span><el-input v-model="form.address" placeholder="请输入派送地址" /></label> <label v-if="props.service === 'relabel'" class="warehouse-span-2"><span><i>*</i>详细派送地址</span><el-input v-model="form.address" placeholder="请输入派送地址" /></label>
<label v-if="props.service === 'transfer' || props.service === 'relabel'"><span><i>*</i>{{ props.service === 'transfer' ? '预估体积(CBM)' : '预估总体积(CBM)' }}</span><el-input-number v-model="form.volumeCbm" :min="0" :precision="4" :step="0.0001" controls-position="right" placeholder="请输入预估总体积" /></label> <label v-if="props.service === 'transfer' || props.service === 'relabel'"><span><i>*</i>{{ props.service === 'transfer' ? '预估体积(CBM)' : '预估总体积(CBM)' }}</span><el-input-number v-model="form.volumeCbm" :min="0" :precision="4" :step="0.0001" controls-position="right" placeholder="请输入预估总体积" /></label>
<label v-if="props.service === 'transfer' || props.service === 'relabel'"><span><i>*</i>{{ props.service === 'transfer' ? '预计入仓时间' : '预计到达时间' }}</span><el-date-picker v-model="form.estimatedArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" :placeholder="props.service === 'transfer' ? '请选择预计入仓时间' : '请选择预计到达时间'" /></label> <label v-if="props.service === 'transfer' || props.service === 'relabel'"><span><i>*</i>{{ props.service === 'transfer' ? '预计入仓时间' : '预计到达时间' }}</span><el-date-picker v-model="form.estimatedArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" :placeholder="props.service === 'transfer' ? '请选择预计入仓时间' : '请选择预计到达时间'" /></label>
<label v-if="props.service === 'transfer'"><span>预计发货时间</span><el-date-picker v-model="form.estimatedShipAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="选填" /></label> <label v-if="props.service === 'transfer'"><span>期望发货时间</span><el-date-picker v-model="form.estimatedShipAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="选填" /></label>
<div v-if="props.service === 'transfer'" class="warehouse-choice-field warehouse-span-2"><span><i>*</i>目的去向</span><el-radio-group v-model="transferDestinationType" @change="selectTransferDestinationType"><el-radio-button value="NONE">暂未确定</el-radio-button><el-radio-button value="FBA">FBA 仓</el-radio-button><el-radio-button value="PRIVATE">私人仓</el-radio-button></el-radio-group></div> <div v-if="props.service === 'transfer'" class="warehouse-choice-field warehouse-span-2"><span><i>*</i>目的去向</span><el-radio-group v-model="transferDestinationType" @change="selectTransferDestinationType"><el-radio-button value="NONE">暂未确定</el-radio-button><el-radio-button value="FBA">FBA 仓</el-radio-button><el-radio-button value="PRIVATE">私人仓</el-radio-button></el-radio-group></div>
<label v-if="props.service === 'transfer' && transferDestinationType === 'FBA'"><span><i>*</i>FBA 目的仓</span><el-select v-model="form.destination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder="请选择 FBA 仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select></label> <label v-if="props.service === 'transfer' && transferDestinationType === 'FBA'"><span><i>*</i>FBA 目的仓</span><el-select v-model="form.destination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder="请选择 FBA 仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select></label>
<label v-if="props.service === 'transfer' && transferDestinationType === 'FBA'"><span>FBA 货件编号</span><el-input v-model="form.fbaShipmentNo" placeholder="选填,可后续补充" /></label> <label v-if="props.service === 'transfer' && transferDestinationType === 'FBA'"><span>FBA 货件编号</span><el-input v-model="form.fbaShipmentNo" placeholder="选填,可后续补充" /></label>
@@ -1328,9 +1433,9 @@ function selectRelabelInventory(inboundOrderId: string) {
<section v-if="canManageRelabelFiles" class="warehouse-file-group warehouse-file-group--primary"> <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 ? '必传 · 两份文件' : '必传 · 二选一' }}</em></div> <div class="warehouse-file-group__heading"><div><strong><span>1</span>贴标总文件</strong></div><em>{{ requiresBothTotalFiles ? '必传 · 两份文件' : '必传 · 二选一' }}</em></div>
<div class="warehouse-file-options"> <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 :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-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-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 :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 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> </div>
</section> </section>
<section v-if="!isReturnRelabelSource" class="warehouse-file-group"> <section v-if="!isReturnRelabelSource" class="warehouse-file-group">
@@ -1346,7 +1451,7 @@ function selectRelabelInventory(inboundOrderId: string) {
<div v-if="isReturnRelabelSource" class="warehouse-table-scroll"> <div v-if="isReturnRelabelSource" class="warehouse-table-scroll">
<table class="warehouse-edit-table warehouse-return-relabel-table"> <table class="warehouse-edit-table warehouse-return-relabel-table">
<thead><tr><th>选择</th><th>SKU</th><th>中文品名</th><th>库存总数</th><th>本次贴标数量</th></tr></thead> <thead><tr><th>选择</th><th>SKU</th><th>中文品名</th><th>库存总数</th><th>本次贴标数量</th></tr></thead>
<tbody><tr v-for="row in returnRelabelSkuRows" :key="row.skuCode"><td><el-checkbox v-model="row.selected" /></td><td><strong>{{ row.skuCode }}</strong></td><td>{{ row.productNameZh || '—' }}</td><td><strong>{{ row.availableQuantity }}</strong></td><td><el-input-number v-model="row.quantity" :disabled="!row.selected" :min="1" :max="row.availableQuantity" :precision="0" controls-position="right" placeholder="请输入贴标数量" /></td></tr><tr v-if="returnRelabelSkuRows.length === 0"><td colspan="5"><div class="warehouse-empty">该退货单暂无可申请贴标的 SKU 库存</div></td></tr></tbody> <tbody><tr v-for="row in returnRelabelSkuRows" :key="row.skuCode"><td><el-checkbox :model-value="row.selected" @change="row.selected = Boolean($event)" /></td><td><strong>{{ row.skuCode }}</strong></td><td>{{ row.productNameZh || '—' }}</td><td><strong>{{ row.availableQuantity }}</strong></td><td><el-input-number v-model="row.quantity" :disabled="!row.selected" :min="1" :max="row.availableQuantity" :precision="0" controls-position="right" placeholder="请输入贴标数量" /></td></tr><tr v-if="returnRelabelSkuRows.length === 0"><td colspan="5"><div class="warehouse-empty">该退货单暂无可申请贴标的 SKU 库存</div></td></tr></tbody>
</table> </table>
</div> </div>
<div v-if="!isReturnRelabelSource" class="warehouse-labeling-mode"> <div v-if="!isReturnRelabelSource" class="warehouse-labeling-mode">
@@ -1382,9 +1487,9 @@ function selectRelabelInventory(inboundOrderId: string) {
<section v-if="canManageRelabelFiles" class="warehouse-file-group warehouse-transfer-total-files"> <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-group__heading"><div><strong>贴标总文件</strong></div><em>{{ requiresBothTotalFiles ? '必传 · 两份文件' : '必传 · 二选一' }}</em></div>
<div class="warehouse-file-options"> <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 :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-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-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 :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 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> </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"> <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>
@@ -1407,10 +1512,17 @@ function selectRelabelInventory(inboundOrderId: string) {
<header class="warehouse-detail-header"> <header class="warehouse-detail-header">
<button type="button" class="warehouse-detail-back" @click="returnToList"><ArrowLeft :size="16" />返回列表</button> <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><span>{{ config.title }}详情</span></div> <div class="warehouse-detail-title"><strong>{{ props.service === 'labeling' ? selectedRelabelDetail?.application.relabelNo : selectedDetail?.transferNo || selectedDetail?.['0'] || `${config.title}详情` }}</strong><span>{{ config.title }}详情</span></div>
<div class="warehouse-detail-actions"><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' && selectedDetail?.status === 'DRAFT'" 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 effect="light">{{ statusLabel(props.service === 'labeling' ? selectedRelabelDetail?.application.status || '' : String(selectedDetail?.status || '')) }}</el-tag></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="canCancelDiscard && props.service === 'disposal' && selectedDetail?.status === 'PENDING_REVIEW'" type="danger" @click="cancelSelectedDiscard">取消申请</el-button><el-button v-if="canCreate && props.service === 'transfer' && selectedDetail?.status === 'DRAFT'" 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 :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> </header>
<div v-if="detailError" class="warehouse-detail-error" role="alert"><strong>{{ detailError }}</strong><el-button @click="loadDetailPage">重新加载</el-button></div> <div v-if="detailError" class="warehouse-detail-error" role="alert"><strong>{{ detailError }}</strong><el-button @click="loadDetailPage">重新加载</el-button></div>
<div v-else 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?.application.itemCount ?? '—' }}</strong></div><div><span>总箱数</span><strong>{{ selectedRelabelDetail?.application.totalPackageCount ?? '—' }}</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 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> <el-alert v-else-if="props.service === 'labeling' && selectedRelabelDetail?.application.status === 'REJECTED'" class="warehouse-rejected-alert" type="error" :closable="false" show-icon title="贴标单审核已驳回" :description="selectedRelabelDetail.application.reviewRemark || '平台未填写驳回备注'" />
<div v-else 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?.application.itemCount ?? '—' }}</strong></div><div><span>总箱数</span><strong>{{ selectedRelabelDetail?.application.totalPackageCount ?? '—' }}</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>{{ 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>箱数</span><strong>{{ selectedDetail?.packageCount ?? '—' }}</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><span>转运单号</span><strong>{{ selectedDetail?.transferNo || '—' }}</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>
<div v-if="props.service === 'transfer'" class="warehouse-card warehouse-form-section warehouse-transfer-measurement-card">
<div class="warehouse-section-heading"><h3>PDA 打托实测数据</h3><span class="warehouse-hint">PDA 作业完成即显示入库完成,客户可确认仓库实测数据</span></div>
<div v-if="selectedDetail?.pallets?.length" class="warehouse-table-scroll"><table><thead><tr><th>托盘号</th><th>库位</th><th>箱数</th><th>长 × 宽 × 高(cm)</th><th>毛重(kg)</th><th>体积(m³)</th><th>状态</th><th>完成时间</th></tr></thead><tbody><tr v-for="pallet in selectedDetail.pallets" :key="String(pallet.id || pallet.sequenceNo)"><td>{{ pallet.palletNo || `第${pallet.sequenceNo || '—'}托` }}</td><td>{{ pallet.warehouseLocationCode || '—' }}</td><td>{{ pallet.cartonCount ?? '—' }}</td><td>{{ [pallet.lengthCm, pallet.widthCm, pallet.heightCm].every((value) => value != null) ? `${pallet.lengthCm} × ${pallet.widthCm} × ${pallet.heightCm}` : '—' }}</td><td>{{ pallet.grossWeightKg ?? '—' }}</td><td>{{ pallet.volumeCbm ?? '—' }}</td><td>{{ statusLabel(pallet.status || '') }}</td><td>{{ formatDateTime(pallet.completedAt) }}</td></tr></tbody></table></div>
<div v-else class="warehouse-empty">暂无 PDA 打托测量数据</div>
<div 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><strong v-else>仓库尚未上传测量打托照片</strong></div>
</div>
<div v-if="props.service === 'transfer'" class="warehouse-card warehouse-form-section warehouse-transfer-cargo-card"> <div v-if="props.service === 'transfer'" class="warehouse-card warehouse-form-section warehouse-transfer-cargo-card">
<div class="warehouse-section-heading"><h3>货物明细</h3><el-button v-if="selectedDetail?.status !== 'DRAFT'" native-type="button" type="primary" @click="printTransferCartons(String(selectedDetail?.transferNo || selectedDetail?.['0'] || ''))"><Printer :size="16" />打印箱唛</el-button></div> <div class="warehouse-section-heading"><h3>货物明细</h3><el-button v-if="selectedDetail?.status !== 'DRAFT'" native-type="button" type="primary" @click="printTransferCartons(String(selectedDetail?.transferNo || selectedDetail?.['0'] || ''))"><Printer :size="16" />打印箱唛</el-button></div>
<div 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="(item, index) in selectedDetail?.items || []" :key="`${item.skuCode}-${index}`"><td>{{ item.skuCode || '—' }}</td><td>{{ item.productNameZh || '—' }}</td><td>{{ item.packageCount ?? '—' }}</td><td>{{ item.shippingMark || '—' }}</td></tr><tr v-if="!selectedDetail?.items?.length"><td colspan="4"><div class="warehouse-empty">暂无货物明细</div></td></tr></tbody></table></div> <div 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="(item, index) in selectedDetail?.items || []" :key="`${item.skuCode}-${index}`"><td>{{ item.skuCode || '—' }}</td><td>{{ item.productNameZh || '—' }}</td><td>{{ item.packageCount ?? '—' }}</td><td>{{ item.shippingMark || '—' }}</td></tr><tr v-if="!selectedDetail?.items?.length"><td colspan="4"><div class="warehouse-empty">暂无货物明细</div></td></tr></tbody></table></div>
@@ -1419,12 +1531,11 @@ function selectRelabelInventory(inboundOrderId: string) {
<h3>换标箱级去向</h3> <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 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>
<div v-if="props.service === 'transfer'" class="warehouse-card warehouse-form-section warehouse-transfer-times"><h3>发货时间</h3><div class="warehouse-detail-grid"><div><span>预计发货时间</span><strong>{{ formatDateTime(selectedDetail?.estimatedShipAt) }}</strong></div><div><span>实际发货时间</span><strong>{{ formatDateTime(selectedDetail?.actualShipAt) }}</strong></div></div><p>实际发货时间由仓库出库或 PDA 完成后回填,客户不能手动修改。</p></div>
<div v-if="props.service === 'labeling' && canManageRelabelFiles" class="warehouse-card warehouse-form-section"> <div v-if="props.service === 'labeling' && canManageRelabelFiles" class="warehouse-card warehouse-form-section">
<div class="warehouse-section-heading"><h3>换标完成与出库</h3></div> <div class="warehouse-section-heading"><h3>换标完成与出库</h3></div>
<div class="warehouse-detail-grid warehouse-fulfillment-grid"> <div class="warehouse-detail-grid warehouse-fulfillment-grid">
<div><span>派送方式</span><strong>{{ deliveryMethodLabel(selectedRelabelDetail?.application.deliveryMethod || '') || '无需出库' }}</strong></div> <div><span>派送方式</span><strong>{{ deliveryMethodLabel(selectedRelabelDetail?.application.deliveryMethod || '') || '无需出库' }}</strong></div>
<div><span>履约状态</span><strong>{{ statusLabel(selectedRelabelDetail?.application.fulfillmentStatus || '') }}</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><span>客户确认时间</span><strong>{{ formatDateTime(selectedRelabelDetail?.application.customerConfirmedAt) }}</strong></div>
</div> </div>
<div 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><strong v-else>仓库尚未上传完工照片</strong></div> <div 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><strong v-else>仓库尚未上传完工照片</strong></div>
@@ -1438,12 +1549,6 @@ function selectRelabelInventory(inboundOrderId: string) {
<tr v-if="!selectedRelabelDetail?.items?.length"><td colspan="5"><div class="warehouse-empty">暂无贴标明细</div></td></tr> <tr v-if="!selectedRelabelDetail?.items?.length"><td colspan="5"><div class="warehouse-empty">暂无贴标明细</div></td></tr>
</tbody></table></div> </tbody></table></div>
</div> </div>
<div v-if="props.service === 'transfer'" class="warehouse-card warehouse-form-section">
<div class="warehouse-section-heading"><h3>PDA 打托实测数据</h3><span class="warehouse-hint">PDA 作业完成即显示入库完成,客户可确认仓库实测数据</span></div>
<div v-if="selectedDetail?.pallets?.length" class="warehouse-table-scroll"><table><thead><tr><th>托盘号</th><th>库位</th><th>箱数</th><th>长 × 宽 × 高(cm)</th><th>毛重(kg)</th><th>体积(m³)</th><th>状态</th><th>完成时间</th></tr></thead><tbody><tr v-for="pallet in selectedDetail.pallets" :key="String(pallet.id || pallet.sequenceNo)"><td>{{ pallet.palletNo || `第${pallet.sequenceNo || '—'}托` }}</td><td>{{ pallet.warehouseLocationCode || '—' }}</td><td>{{ pallet.cartonCount ?? '—' }}</td><td>{{ [pallet.lengthCm, pallet.widthCm, pallet.heightCm].every((value) => value != null) ? `${pallet.lengthCm} × ${pallet.widthCm} × ${pallet.heightCm}` : '—' }}</td><td>{{ pallet.grossWeightKg ?? '—' }}</td><td>{{ pallet.volumeCbm ?? '—' }}</td><td>{{ statusLabel(pallet.status || '') }}</td><td>{{ formatDateTime(pallet.completedAt) }}</td></tr></tbody></table></div>
<div v-else class="warehouse-empty">暂无 PDA 打托测量数据</div>
<div class="warehouse-evidence warehouse-measurement-evidence"><span>测量打托现场照片</span><div v-if="selectedDetail?.measurementPhotos?.length" class="warehouse-photo-grid"><div v-for="(photo, index) in selectedDetail.measurementPhotos" :key="photo.fileId" class="warehouse-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><strong v-else>仓库尚未上传测量打托照片</strong></div>
</div>
<div v-if="props.service === 'labeling'" class="warehouse-card warehouse-form-section"> <div v-if="props.service === 'labeling'" class="warehouse-card warehouse-form-section">
<h3>换标任务明细</h3> <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> <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>
@@ -1474,6 +1579,8 @@ function selectRelabelInventory(inboundOrderId: string) {
.warehouse-service-page--return-relabel .warehouse-table-scroll:has(> .warehouse-return-relabel-table)~.warehouse-table-scroll{display:none} .warehouse-service-page--return-relabel .warehouse-table-scroll:has(> .warehouse-return-relabel-table)~.warehouse-table-scroll{display:none}
.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-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-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:180px}
.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-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-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,1fr) minmax(0,1.35fr);align-items:start}.warehouse-transfer-form--relabel .warehouse-transfer-main-card{grid-column:1;grid-row:1}.warehouse-transfer-form--relabel .warehouse-transfer-cargo-card{grid-column:1;grid-row:2}.warehouse-transfer-form--relabel .warehouse-transfer-relabel{grid-column:2;grid-row:1 / span 2;margin-bottom:0}.warehouse-transfer-form--relabel .warehouse-transfer-submit-bar{grid-column:1 / -1}.warehouse-transfer-form :deep(.el-input__wrapper),.warehouse-transfer-form :deep(.el-textarea__inner),.warehouse-transfer-form :deep(.el-select__wrapper),.warehouse-transfer-form :deep(.el-date-editor.el-input__wrapper){min-height:32px}.warehouse-transfer-form .warehouse-form-section{padding:16px}.warehouse-transfer-form .warehouse-form-section h3{margin-bottom:12px;padding-bottom:9px;font-size:14px}.warehouse-transfer-form .warehouse-form-grid{gap:12px 16px}.warehouse-transfer-form .warehouse-choice-field{min-height:34px;padding:8px 10px;grid-template-columns:118px 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{display:grid;grid-template-columns:minmax(0,1fr);gap:12px}.warehouse-transfer-form--relabel{grid-template-columns:minmax(0,1fr) minmax(0,1.35fr);align-items:start}.warehouse-transfer-form--relabel .warehouse-transfer-main-card{grid-column:1;grid-row:1}.warehouse-transfer-form--relabel .warehouse-transfer-cargo-card{grid-column:1;grid-row:2}.warehouse-transfer-form--relabel .warehouse-transfer-relabel{grid-column:2;grid-row:1 / span 2;margin-bottom:0}.warehouse-transfer-form--relabel .warehouse-transfer-submit-bar{grid-column:1 / -1}.warehouse-transfer-form :deep(.el-input__wrapper),.warehouse-transfer-form :deep(.el-textarea__inner),.warehouse-transfer-form :deep(.el-select__wrapper),.warehouse-transfer-form :deep(.el-date-editor.el-input__wrapper){min-height:32px}.warehouse-transfer-form .warehouse-form-section{padding:16px}.warehouse-transfer-form .warehouse-form-section h3{margin-bottom:12px;padding-bottom:9px;font-size:14px}.warehouse-transfer-form .warehouse-form-grid{gap:12px 16px}.warehouse-transfer-form .warehouse-choice-field{min-height:34px;padding:8px 10px;grid-template-columns:118px 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}
@@ -1482,6 +1589,11 @@ function selectRelabelInventory(inboundOrderId: string) {
@media(max-width:1180px){.warehouse-transfer-file-groups{grid-template-columns:minmax(0,1fr)}} @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-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-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}
@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. */ /* Keep the count and controls visible together on narrow list pages. */
.warehouse-pagination { justify-content: flex-end; } .warehouse-pagination { justify-content: flex-end; }
.warehouse-pagination-total { color: #526176; font-size: 14px; } .warehouse-pagination-total { color: #526176; font-size: 14px; }
@@ -208,10 +208,11 @@ test('贴标详情展示箱唛、SKU 子明细和独立FBA目的仓', () => {
assert.match(view, /item\.skuItems\?\.length/) assert.match(view, /item\.skuItems\?\.length/)
}) })
test('客户在换标列表确认完工且照片使用二进制下载', () => { test('客户可在换标列表或详情确认完工且照片使用二进制下载', () => {
assert.match(view, /item\.__status === 'CUSTOMER_CONFIRM_PENDING'/) assert.match(view, /item\.__status === 'CUSTOMER_CONFIRM_PENDING'/)
assert.match(view, /confirmRelabelFromList\(item\['0'\]\)/) assert.match(view, /confirmRelabelFromList\(item\['0'\]\)/)
assert.doesNotMatch(view, /selectedRelabelDetail\?\.application\.status === 'CUSTOMER_CONFIRM_PENDING'/) assert.match(view, /selectedRelabelDetail\?\.application\.status === 'CUSTOMER_CONFIRM_PENDING'/)
assert.match(view, /@click="confirmSelectedRelabel">确认换标完成/)
assert.match(api, /overseasBlobRequest/) assert.match(api, /overseasBlobRequest/)
}) })
@@ -224,6 +225,14 @@ test('客户详情进入页面后直接回显完工照片并支持大图预览',
}) })
test('进入复核后客户订单回显换标完成状态', () => { test('进入复核后客户订单回显换标完成状态', () => {
assert.match(view, /value === 'CUSTOMER_CONFIRM_PENDING'\) return '待客户确认'/) assert.match(view, /CUSTOMER_CONFIRM_PENDING: '待客户确认'/)
assert.match(view, /\['READY_FOR_OUTBOUND', 'COMPLETED'\]\.includes\(value\) \? '换标完成'/) assert.match(view, /READY_FOR_OUTBOUND: '换标完成'/)
})
test('审核驳回后展示原因并允许替换资料后重新提交', () => {
assert.match(api, /export function resubmitRelabel\(/)
assert.match(view, /贴标单审核已驳回/)
assert.match(view, /application\.reviewRemark/)
assert.match(view, /修改后重新提交/)
assert.match(view, /await resubmitRelabel\(/)
}) })
@@ -40,6 +40,17 @@ test('仓储转运支持无目的地、FBA 仓和私人仓并显式选择是否
assert.doesNotMatch(view, /relabelChargeAmount:/) assert.doesNotMatch(view, /relabelChargeAmount:/)
}) })
test('换标详情待客户确认时提供确认入口并刷新详情', () => {
assert.match(view, /application\.status === 'CUSTOMER_CONFIRM_PENDING'[\s\S]*@click="confirmSelectedRelabel">确认换标完成/)
assert.match(view, /async function confirmSelectedRelabel\(\)[\s\S]*await confirmRelabelCompletion\(relabelNo\)[\s\S]*await openDetail\(\{ '0': relabelNo \}\)/)
})
test('仓储转运换标绑定明细自动同步只读的旧 SKU', () => {
assert.match(view, /watch\(\(\) => cargoRows\.value\.map\(\(row\) => row\.sku\)/)
assert.match(view, /if \(sourceSku\) row\.sku = sourceSku/)
assert.match(view, /transferRelabelRows\.value\.push\(\{ \.\.\.createCargoRow\(\), sku: source\?\.sku \|\| '' \}\)/)
})
test('仓储转运换标整票只允许一个目的仓', () => { test('仓储转运换标整票只允许一个目的仓', () => {
assert.match(view, /props\.service === 'transfer' && transferDestinationType === 'FBA'[^\n]+FBA 目的仓/) assert.match(view, /props\.service === 'transfer' && transferDestinationType === 'FBA'[^\n]+FBA 目的仓/)
assert.match(view, /FBA 货件编号<\/span><el-input v-model="form\.fbaShipmentNo" placeholder="选填,可后续补充"/) assert.match(view, /FBA 货件编号<\/span><el-input v-model="form\.fbaShipmentNo" placeholder="选填,可后续补充"/)
@@ -120,6 +131,8 @@ test('仓储转运需要换标时上传总文件并挂到关联换标单', () =>
assert.match(view, /uploadRelabelFile\(relabelNo, 'CARTON_TOTAL', cartonTotalFile\.value\)/) assert.match(view, /uploadRelabelFile\(relabelNo, 'CARTON_TOTAL', cartonTotalFile\.value\)/)
assert.match(view, /uploadRelabelFile\(relabelNo, 'SKU_TOTAL', skuTotalFile\.value\)/) assert.match(view, /uploadRelabelFile\(relabelNo, 'SKU_TOTAL', skuTotalFile\.value\)/)
assert.match(view, /转运单已提交,附件待补传/) assert.match(view, /转运单已提交,附件待补传/)
assert.match(transferRelabelSection, /native-type="button"[^>]+chooseCreateTotalFile\('CARTON_TOTAL'\)/)
assert.match(transferRelabelSection, /native-type="button"[^>]+chooseCreateTotalFile\('SKU_TOTAL'\)/)
}) })
test('仓储转运换标总文件与明细模板在桌面端左右排列', () => { test('仓储转运换标总文件与明细模板在桌面端左右排列', () => {
@@ -155,13 +168,17 @@ test('仓储转运列表操作仓使用海外仓接口数据', () => {
assert.doesNotMatch(view, /美西洛杉矶仓/) assert.doesNotMatch(view, /美西洛杉矶仓/)
}) })
test('仓储转运预计发货时间可选并只读展示实际发货时间', () => { test('仓储转运期望发货时间可选并在订单概览回填发货时间', () => {
assert.match(view, /预计发货时间/) assert.doesNotMatch(view, /预计发货时间/)
assert.match(view, /期望发货时间/)
assert.match(view, /v-model="form\.estimatedShipAt"/) assert.match(view, /v-model="form\.estimatedShipAt"/)
assert.match(view, /v-model="form\.estimatedShipAt"[^>]+placeholder="选填"/) assert.match(view, /v-model="form\.estimatedShipAt"[^>]+placeholder="选填"/)
assert.doesNotMatch(view, /!form\.estimatedShipAt && '预计发货时间'/) assert.doesNotMatch(view, /!form\.estimatedShipAt/)
assert.match(view, /estimatedShipAt: form\.estimatedShipAt/) assert.match(view, /estimatedShipAt: form\.estimatedShipAt/)
assert.match(view, /实际发货时间由仓库出库或 PDA 完成后回填/) assert.match(view, /<span>期望发货时间<\/span><strong>\{\{ formatDateTime\(selectedDetail\?\.estimatedShipAt\) \}\}<\/strong>/)
assert.match(view, /<span>实际发货时间<\/span><strong>\{\{ formatDateTime\(selectedDetail\?\.actualShipAt\) \}\}<\/strong>/)
assert.doesNotMatch(view, /class="warehouse-card warehouse-form-section warehouse-transfer-times"/)
assert.match(view, /estimatedArrivalAt: detail\.estimatedArrivalAt \|\| '', estimatedShipAt: detail\.estimatedShipAt \|\| ''/)
assert.match(api, /estimatedShipAt\?: string/) assert.match(api, /estimatedShipAt\?: string/)
}) })
@@ -176,6 +193,66 @@ test('客户仓储转运展示每个实际流转状态的中文名称', () => {
assert.match(view, /OUTBOUNDED: '已出库'/) assert.match(view, /OUTBOUNDED: '已出库'/)
}) })
test('仓储转运列表展示换标单号、换标状态并为两类状态着色', () => {
const transferConfig = view.match(/transfer:\s*\{[^\n]+/)?.[0] || ''
assert.match(transferConfig, /'是否换标', '换标单号', '换标状态', '状态'/)
assert.match(api, /relabelNo\?: string; relabelStatus\?: string; relabelFulfillmentStatus\?: string/)
assert.match(view, /item\.needRelabel \? '是' : '否', item\.relabelNo, relabelStatus\.label/)
assert.match(view, /if \(!item\.relabelNo\) return \{ code: 'WAIT_RELABEL', label: '待换标' \}/)
assert.match(view, /READY_FOR_OUTBOUND: '已换标', COMPLETED: '已换标'/)
assert.match(view, /#transferRelabelStatus=.*transferRelabelStatusTagType/)
assert.match(view, /#transferStatus=.*transferStatusTagType/)
assert.doesNotMatch(view, /是 · 待绑定换标单/)
})
test('仓储转运详情订单概览使用详情接口命名字段回填', () => {
assert.match(view, /v-else-if="props\.service === 'transfer'" class="warehouse-detail-grid warehouse-transfer-overview-grid"/)
assert.doesNotMatch(view, /入库箱数/)
assert.match(view, /'转运单号', '箱数', '海外仓'/)
for (const field of ['transferNo', 'packageCount', 'operatingWarehouseId', 'deliveryMethod', 'needRelabel', 'relabelNo', 'status', 'createdAt']) {
assert.match(view, new RegExp(`selectedDetail\\?\\.${field}`))
}
assert.match(view, /selectedDetail\?\.destinationWarehouseCode \|\| selectedDetail\?\.fbaWarehouseCode/)
assert.match(view, /transferRelabelStatus\(selectedDetail \|\| \{\}\)/)
assert.match(view, /transferStatusTagType\(selectedDetail\?\.status \|\| ''\)/)
})
test('仓储转运和贴标申请详情先展示主数据再串行补充仓库名称', () => {
const loader = view.match(/async function loadDetailPage\(\) \{[\s\S]*?\n\}/)?.[0] || ''
assert.match(loader, /await openDetail\(\{ '0': detailNumber\.value \}\)/)
assert.match(loader, /await loadWarehouses\(\)/)
assert.ok(loader.indexOf('await openDetail') < loader.indexOf('await loadWarehouses'))
assert.match(view, /props\.service === 'labeling' \? await getRelabel\(value\['0'\]\)/)
assert.match(view, /: props\.service === 'disposal' \? await getDiscard\(value\['0'\]\) : await getTransfer\(value\['0'\]\)/)
})
test('仓储转运详情展示目的去向和对应目的仓资料并让货物明细铺满', () => {
assert.match(view, />目的去向<\/span><strong>\{\{ destinationTypeLabel\(selectedDetail\?\.destinationType, selectedDetail\?\.fbaWarehouseCode\) \}\}/)
assert.match(view, />FBA 目的仓<\/span>.*selectedDetail\?\.destinationWarehouseCode \|\| selectedDetail\?\.fbaWarehouseCode/)
assert.match(view, />FBA 货件号<\/span><strong>\{\{ selectedDetail\?\.fbaShipmentNo \|\| '—' \}\}/)
for (const field of ['privateDestinationAddress', 'privateDestinationPostalCode', 'privateDestinationContactName', 'privateDestinationContactPhone']) {
assert.match(view, new RegExp(`selectedDetail\\?\\.${field}`))
}
assert.match(view, /warehouse-service-page--detail \.warehouse-transfer-cargo-card \.warehouse-section-heading,[^}]+\.warehouse-table-scroll\{max-width:none\}/)
})
test('仓储转运订单概览使用紧凑左右信息布局', () => {
assert.match(view, /warehouse-detail-grid warehouse-transfer-overview-grid/)
assert.match(view, /warehouse-transfer-overview-grid"><div><span>状态<\/span>.*<span>目的去向<\/span>/)
assert.match(view, /class="warehouse-overview-wide"><span>私人仓地址/)
assert.match(view, /warehouse-transfer-overview-grid>\.warehouse-overview-wide\{grid-column:span 2\}/)
assert.match(view, /warehouse-transfer-overview-grid>div\{min-height:26px;padding:2px 0;border:0\}/)
assert.match(view, /warehouse-transfer-overview-grid span\{flex:0 0 auto;min-width:76px\}/)
assert.match(view, /warehouse-transfer-overview-grid\{grid-template-columns:1fr;gap:4px/)
})
test('贴标申请详情使用紧凑订单概览并中文展示换标状态', () => {
assert.match(view, /WAIT_APPROVAL: '待审核'/)
assert.match(view, /WAIT_PROCESSING: '待仓库处理'/)
assert.match(view, /relabelFulfillmentLabel\(selectedRelabelDetail\?\.application\.fulfillmentStatus\)/)
assert.match(view, />\.warehouse-detail-header\+\.warehouse-card \.warehouse-detail-grid\{grid-template-columns:repeat\(4,minmax\(180px,1fr\)\)/)
})
test('PDA 打托完成后客户可从列表或详情确认并查看实测数据', () => { test('PDA 打托完成后客户可从列表或详情确认并查看实测数据', () => {
assert.match(view, /item\.__status === 'WAIT_CUSTOMER_CONFIRMATION'/) assert.match(view, /item\.__status === 'WAIT_CUSTOMER_CONFIRMATION'/)
assert.match(view, /confirmTransferFromList\(item\['0'\]\)/) assert.match(view, /confirmTransferFromList\(item\['0'\]\)/)
@@ -189,6 +266,20 @@ test('PDA 打托完成后客户可从列表或详情确认并查看实测数据'
assert.doesNotMatch(view, /pallet\.items/) assert.doesNotMatch(view, /pallet\.items/)
}) })
test('仓储转运详情按订单概览、PDA 实测数据、货物明细排序并放大测量照片', () => {
const detailStart = view.indexOf('<header class="warehouse-detail-header">')
const detailSource = view.slice(detailStart)
const overviewIndex = detailSource.indexOf('<h3>订单概览</h3>')
const measurementIndex = detailSource.indexOf('<h3>PDA 打托实测数据</h3>')
const cargoIndex = detailSource.indexOf('<h3>货物明细</h3>')
assert.ok(overviewIndex >= 0 && overviewIndex < measurementIndex)
assert.ok(measurementIndex < cargoIndex)
assert.match(view, /class="warehouse-photo-grid warehouse-measurement-photo-grid"/)
assert.match(view, /class="warehouse-photo-item warehouse-measurement-photo-item"/)
assert.match(view, /warehouse-measurement-photo-grid\{flex:1;grid-template-columns:repeat\(auto-fill,minmax\(240px,1fr\)\)\}/)
assert.match(view, /warehouse-measurement-photo-item\{width:100%;height:180px\}/)
})
test('仓储转运详情自动加载 PDA 测量照片并释放 Blob URL', () => { test('仓储转运详情自动加载 PDA 测量照片并释放 Blob URL', () => {
assert.match(api, /downloadTransferMeasurementPhoto/) assert.match(api, /downloadTransferMeasurementPhoto/)
assert.match(api, /measurement-photos\/files\/\$\{encodeURIComponent\(fileId\)\}\/download/) assert.match(api, /measurement-photos\/files\/\$\{encodeURIComponent\(fileId\)\}\/download/)
@@ -229,6 +320,12 @@ test('仓储转运可在新建页直接提交,也可保存草稿后编辑提
assert.doesNotMatch(view, /<h3>箱唛明细<\/h3>/) assert.doesNotMatch(view, /<h3>箱唛明细<\/h3>/)
}) })
test('转运贴标单在PDA确认库存前不误报附件上传失败', () => {
assert.match(view, /if \(!relabelNo\) \{[\s\S]*PDA 入库确认后将自动生成关联换标单/)
assert.match(view, /届时请进入关联换标单详情上传贴标总文件/)
assert.doesNotMatch(view, /if \(!relabelNo\) throw new Error\('转运单已提交,但未返回关联换标单号'\)/)
})
test('仓储转运基础货物不强制 SKU 和中文品名,只换箱唛不强制旧 SKU', () => { test('仓储转运基础货物不强制 SKU 和中文品名,只换箱唛不强制旧 SKU', () => {
assert.match(view, /商品 SKU\{\{ props\.service === 'transfer' \? '' : ' \*' \}\}/) assert.match(view, /商品 SKU\{\{ props\.service === 'transfer' \? '' : ' \*' \}\}/)
assert.match(view, /中文品名\{\{ props\.service === 'transfer' \? '' : ' \*' \}\}/) assert.match(view, /中文品名\{\{ props\.service === 'transfer' \? '' : ' \*' \}\}/)