merge: preserve remote client updates and truck demand contract
This commit is contained in:
@@ -43,6 +43,8 @@ const visibleNavigation = computed(() => filterNavigationByPermissions(clientNav
|
||||
|
||||
const activeParentKeys = computed(() => new Set(getNavigationParentKeys(visibleNavigation.value, props.activePath)))
|
||||
const openedKeys = ref<ReadonlySet<string>>(new Set(activeParentKeys.value))
|
||||
// Expand the order group by default so the three customer order shortcuts are visible.
|
||||
openedKeys.value = new Set([...openedKeys.value, 'order-center'])
|
||||
|
||||
function handleToggle(key: string) {
|
||||
const next = new Set(openedKeys.value)
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { Package, Truck } from '@lucide/vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
||||
import type { TableAction } from '@/components/common/tableActions'
|
||||
import { useAuthStore } from '@/modules/auth/public'
|
||||
import { getMerchantWarehouses, type MerchantWarehouse } from '@/modules/first-leg/public'
|
||||
import {
|
||||
createTransport,
|
||||
getBusinessOrder,
|
||||
@@ -58,6 +60,9 @@ interface BusinessForm {
|
||||
contactInfo: string
|
||||
requestedDate: string
|
||||
remark: string
|
||||
cargoName: string
|
||||
deliveryDate: string
|
||||
palletCount: number
|
||||
}
|
||||
|
||||
const configuration = computed(() => ({
|
||||
@@ -102,6 +107,10 @@ const fleetArrangement = computed(() => detail.value ? (detail.value as Transpor
|
||||
const formRef = ref<FormInstance>()
|
||||
const query = reactive({ page: 1, size: 20, orderNo: '', routeKeyword: '', status: '' })
|
||||
const total = ref(0)
|
||||
const overseasWarehouses = ref<MerchantWarehouse[]>([])
|
||||
const overseasWarehousesLoading = ref(false)
|
||||
const overseasWarehousesError = ref('')
|
||||
const isSelfDelivery = computed(() => props.kind === 'looseboard' && form.transportMode === 'SHARED_TRUCK')
|
||||
|
||||
const queryModel = computed<QueryModel>({
|
||||
get: () => ({
|
||||
@@ -201,11 +210,30 @@ function emptyForm(): BusinessForm {
|
||||
transportMode: 'SHARED_TRUCK', palletRequired: 'NO', transportType: 'PORT_PICKUP', origin: '', originAddress: '',
|
||||
destination: '', destinationAddress: '', containerType: DEFAULT_CONTAINER_TYPE, packageCount: 0,
|
||||
totalWeightKg: 0, totalVolumeCbm: 0, currency: 'USD', contactName: '', contactInfo: '', requestedDate: '',
|
||||
remark: '', vehicleType: '', originContactName: '', originContactPhone: '', destinationContactName: '', destinationContactPhone: '', pickupAt: '', deliveryAt: '',
|
||||
remark: '', cargoName: '', deliveryDate: '', palletCount: 0, vehicleType: '', originContactName: '', originContactPhone: '', destinationContactName: '', destinationContactPhone: '', pickupAt: '', deliveryAt: '',
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() { Object.assign(form, emptyForm()); submitKey.value = ''; formRef.value?.clearValidate() }
|
||||
function warehouseLabel(warehouse: MerchantWarehouse) { return `${warehouse.warehouseName} · ${warehouse.warehouseCode}` }
|
||||
function selectOriginMode() {
|
||||
form.origin = ''
|
||||
form.originAddress = ''
|
||||
formRef.value?.clearValidate('origin')
|
||||
}
|
||||
async function loadOverseasWarehouses() {
|
||||
if (props.kind !== 'looseboard') return
|
||||
overseasWarehousesLoading.value = true
|
||||
overseasWarehousesError.value = ''
|
||||
try {
|
||||
const result = await getMerchantWarehouses({ page: 1, size: 200, warehouseType: 'OVERSEAS' })
|
||||
overseasWarehouses.value = result.records.filter(item => item.status === 'ACTIVE' && ['OVERSEAS', 'OVERSEAS_PARTNER'].includes(item.warehouseType) && item.countryCode === 'US')
|
||||
if (!overseasWarehouses.value.length) overseasWarehousesError.value = '暂无可用的海外仓'
|
||||
} catch (cause) {
|
||||
overseasWarehouses.value = []
|
||||
overseasWarehousesError.value = errorMessage(cause)
|
||||
} finally { overseasWarehousesLoading.value = false }
|
||||
}
|
||||
function read(source: object, key: string) { return Reflect.get(source, key) as unknown }
|
||||
function text(value: unknown) { return value === null || value === undefined || value === '' ? '—' : String(value) }
|
||||
function money(amount: unknown, currency: unknown) { return amount === null || amount === undefined ? '—' : `${text(currency)} ${Number(amount).toFixed(2)}` }
|
||||
@@ -319,7 +347,7 @@ async function submit() {
|
||||
packageCount: form.packageCount,
|
||||
totalWeightKg: form.totalWeightKg, totalVolumeCbm: form.totalVolumeCbm,
|
||||
budgetAmount: form.budgetAmount, currency: form.currency || undefined,
|
||||
remark: form.remark || undefined,
|
||||
remark: props.kind === 'looseboard' ? [form.cargoName, form.remark].filter(Boolean).join(' · ') || undefined : form.remark || undefined,
|
||||
idempotencyKey: stableSubmitKey,
|
||||
}
|
||||
if (props.kind === 'trucking') {
|
||||
@@ -344,7 +372,7 @@ async function submit() {
|
||||
}
|
||||
|
||||
watch(() => [query.page, query.size], () => { if (!props.createPage && mode.value === 'list') void load() })
|
||||
onMounted(() => { if (props.detailPage) void loadDetail(); else if (!props.createPage) void load() })
|
||||
onMounted(() => { if (props.detailPage) void loadDetail(); else if (!props.createPage) void load(); void loadOverseasWarehouses() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -402,6 +430,42 @@ onMounted(() => { if (props.detailPage) void loadDetail(); else if (!props.creat
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<el-card v-else-if="kind === 'looseboard'" shadow="never" class="form-card looseboard-form-card">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
|
||||
<div class="form-section transport-section">
|
||||
<h3><span class="section-icon"><Truck :size="14" /></span>运输方式 <em>*</em></h3>
|
||||
<div class="mode-buttons"><el-radio-group v-model="form.transportMode" @change="selectOriginMode"><el-radio-button value="SHARED_TRUCK"><Truck :size="14" /> 自送</el-radio-button><el-radio-button value="PRIORITY_CONSOLIDATION"><Package :size="14" /> 我司提货</el-radio-button></el-radio-group></div>
|
||||
</div>
|
||||
<div class="form-grid two-col">
|
||||
<el-form-item :label="isSelfDelivery ? '入仓库' : '取货地'" prop="origin" :rules="[{ required: true, message: isSelfDelivery ? '请选择美国仓库' : '请输入取货地', trigger: isSelfDelivery ? 'change' : 'blur' }]">
|
||||
<template v-if="isSelfDelivery">
|
||||
<el-select v-model="form.origin" filterable clearable :loading="overseasWarehousesLoading" placeholder="请选择美国仓库" no-data-text="暂无可用的美国海外仓">
|
||||
<el-option v-for="warehouse in overseasWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouseLabel(warehouse)" />
|
||||
</el-select>
|
||||
<div v-if="overseasWarehousesError" role="alert" class="warehouse-load-error">{{ overseasWarehousesError }} <el-button link type="primary" @click="loadOverseasWarehouses">重新加载</el-button></div>
|
||||
</template>
|
||||
<el-input v-else v-model.trim="form.origin" maxlength="255" placeholder="城市 / 港口 / 州" />
|
||||
</el-form-item>
|
||||
<el-form-item label="目的地" prop="destination"><el-input v-model.trim="form.destination" maxlength="255" placeholder="城市 / 州" /></el-form-item>
|
||||
<el-form-item label="联系人" prop="contactName"><el-input v-model.trim="form.contactName" maxlength="128" placeholder="联系人姓名" /></el-form-item>
|
||||
<el-form-item label="联系方式" prop="contactInfo"><el-input v-model.trim="form.contactInfo" maxlength="128" placeholder="联系电话" /></el-form-item>
|
||||
</div>
|
||||
<div class="form-section cargo-section"><h3><span class="section-icon"><Package :size="14" /></span>托载</h3><div class="form-grid cargo-grid">
|
||||
<el-form-item label="商品名称"><el-input v-model.trim="form.cargoName" placeholder="请输入商品名称" /></el-form-item>
|
||||
<el-form-item label="托数"><el-input-number v-model="form.palletCount" :min="0" controls-position="right" /></el-form-item>
|
||||
<el-form-item label="总重量 (kg)"><el-input-number v-model="form.totalWeightKg" :min="0" :precision="2" controls-position="right" /></el-form-item>
|
||||
<el-form-item label="总体积 (m³)"><el-input-number v-model="form.totalVolumeCbm" :min="0" :precision="3" controls-position="right" /></el-form-item>
|
||||
</div></div>
|
||||
<div class="form-grid two-col lower-grid">
|
||||
<el-form-item label="预算总价($)"><el-input-number v-model="form.budgetAmount" :min="0" :precision="2" controls-position="right" placeholder="0.00" /></el-form-item>
|
||||
<div></div>
|
||||
<el-form-item label="要求取货时间" prop="requestedDate"><el-date-picker v-model="form.requestedDate" type="date" value-format="YYYY-MM-DD" placeholder="年 / 月 / 日" /></el-form-item>
|
||||
<el-form-item label="送货到达时间"><el-date-picker v-model="form.deliveryDate" type="date" value-format="YYYY-MM-DD" placeholder="年 / 月 / 日" /></el-form-item>
|
||||
</div>
|
||||
<div class="form-actions"><el-button @click="returnToList">取消</el-button><el-button v-if="canCreate" type="primary" :loading="submitting" @click="submit">提交订单</el-button></div>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card v-else shadow="never" class="form-card">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" :label-width="kind === 'trucking' ? '140px' : '120px'">
|
||||
<el-form-item v-if="kind === 'looseboard'" label="运输方式"><el-radio-group v-model="form.transportMode"><el-radio-button value="SHARED_TRUCK">拼车</el-radio-button><el-radio-button value="PRIORITY_CONSOLIDATION">优先拼载</el-radio-button></el-radio-group></el-form-item>
|
||||
@@ -469,6 +533,32 @@ onMounted(() => { if (props.detailPage) void loadDetail(); else if (!props.creat
|
||||
.form-card :deep(.el-input), .form-card :deep(.el-textarea), .form-card :deep(.el-select), .form-card :deep(.el-date-editor) { width: 100%; }
|
||||
.form-card .currency { width: 110px; margin-left: 12px; }
|
||||
|
||||
.warehouse-load-error { color: #b42318; font-size: 12px; }
|
||||
.looseboard-form-card { padding: 0; border: 1px solid #e7ebf1; border-radius: 10px; background: #fff; box-shadow: 0 8px 22px rgb(31 45 61 / 4%); }
|
||||
.looseboard-form-card :deep(.el-card__body) { padding: 0; }
|
||||
.looseboard-form-card :deep(.el-form) { max-width: none; }
|
||||
.looseboard-form-card :deep(.el-form-item) { margin-bottom: 16px; }
|
||||
.looseboard-form-card :deep(.el-form-item__label) { height: auto; padding-bottom: 7px; color: #27364d; font-size: 13px; font-weight: 600; line-height: 1.2; }
|
||||
.looseboard-form-card :deep(.el-input__wrapper), .looseboard-form-card :deep(.el-textarea__inner), .looseboard-form-card :deep(.el-select__wrapper), .looseboard-form-card :deep(.el-input-number) { min-height: 36px; border-radius: 8px; box-shadow: 0 0 0 1px #e1e6ed inset; }
|
||||
.looseboard-form-card :deep(.el-input-number) { width: 100%; }
|
||||
.form-section { padding: 25px 30px 5px; }
|
||||
.form-section h3 { display: flex; align-items: center; gap: 7px; margin: 0 0 15px; color: #27364d; font-size: 14px; }
|
||||
.form-section h3 em { color: #e05252; font-style: normal; }
|
||||
.section-icon { display: inline-flex; width: 20px; height: 20px; align-items: center; justify-content: center; border-radius: 5px; background: #fff3e0; color: #dd8d2c; }
|
||||
.mode-buttons :deep(.el-radio-group) { display: flex; gap: 10px; }
|
||||
.mode-buttons :deep(.el-radio-button__inner) { display: inline-flex; align-items: center; gap: 6px; height: 36px; padding: 0 18px; border: 1px solid #dce3eb; border-radius: 8px !important; color: #4b5563; box-shadow: none; }
|
||||
.mode-buttons :deep(.el-radio-button:first-child .el-radio-button__inner) { border-left: 1px solid #dce3eb; }
|
||||
.mode-buttons :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) { border-color: #5965d8; background: #f0f2ff; color: #4f5bd5; box-shadow: none; }
|
||||
.form-grid { display: grid; gap: 0 22px; padding: 0 30px; }
|
||||
.two-col { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.cargo-section { margin-top: 4px; padding-top: 7px; background: #fafafa; border-top: 1px solid #f0f2f5; border-bottom: 1px solid #f0f2f5; }
|
||||
.cargo-grid { grid-template-columns: 1.25fr 1.25fr 1.25fr 1.25fr; }
|
||||
.lower-grid { padding-top: 20px; }
|
||||
.lower-grid :deep(.el-date-editor) { width: 100%; }
|
||||
.form-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 5px; padding: 20px 30px 25px; border-top: 1px solid #f0f2f5; }
|
||||
.form-actions .el-button { min-width: 80px; height: 36px; border-radius: 7px; }
|
||||
.form-actions .el-button--primary { background: linear-gradient(135deg, #6774df, #7553aa); border: 0; box-shadow: 0 5px 12px rgb(103 116 223 / 24%); }
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.business-orders { gap: 14px; }
|
||||
.page-head { align-items: flex-start; gap: 12px; }
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { apiRequest, apiBlobRequest } from '@/api/http'
|
||||
|
||||
export interface PickupProgress {
|
||||
status: string; tripNo: string | null; vehiclePlate: string | null; driverName: string | null; driverPhone: string | null
|
||||
plannedDepartureAt: string | null; actualPackages: number | null; pickedAt: string | null; arrivedAt: string | null; remark: string | null
|
||||
evidence: { fileId: string; fileName: string }[]
|
||||
}
|
||||
/** 客户范围由统一会话确定,不允许调用方传入其他客户。 */
|
||||
export function getTransferPickupProgress(transferNo: string) {
|
||||
return apiRequest<PickupProgress | null>({ method: 'GET', url: `/merchant/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}/pickup-progress` })
|
||||
}
|
||||
/** 只下载本票已经确认的凭证。 */
|
||||
export function downloadTransferPickupEvidence(transferNo: string, fileId: string) {
|
||||
return apiBlobRequest({ method: 'GET', url: `/merchant/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}/pickup-evidence/${encodeURIComponent(fileId)}` })
|
||||
}
|
||||
@@ -48,7 +48,7 @@ export interface TransferCreate {
|
||||
deliveryAddress?: string; packageCount: number; volumeCbm: number; deliveryMethod?: string; estimatedArrivalAt?: string; estimatedShipAt?: string
|
||||
fbaShipmentNo?: string; trackingNo?: string; fbaBoxNos?: string; remark?: string; needRelabel: boolean; idempotencyKey: string
|
||||
labelDeadline?: string; pickupContactName?: string; pickupContactPhone?: string; relabelContactName?: string; relabelContactPhone?: string; relabelChargeAmount?: number; relabelDeliveryMethod?: RelabelDeliveryMethod
|
||||
items: Array<{ skuCode?: string; productNameZh?: string; quantity: number; packageCount: number; shippingMark?: string }>
|
||||
items: Array<{ skuCode?: string; productNameZh?: string; quantity: number; packageCount: number; shippingMark?: string; cartonMarks?: string[] }>
|
||||
relabelItems?: Array<{ operationType: 'CARTON_LABEL' | 'SKU_LABEL' | 'CARTON_AND_SKU'; customerCartonMarks: string[]; originalSku?: string; newSku?: string; quantity: number; fbaCartonMark?: string; destinationWarehouseCode?: string; remark?: string }>
|
||||
}
|
||||
export interface RelabelCreate {
|
||||
@@ -77,7 +77,7 @@ export function submitTransfer(transferNo: string) {
|
||||
return apiRequest<Record<string, unknown>>({ method: 'POST', url: `/merchant/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}/submit` })
|
||||
}
|
||||
/** 补充既有SKU的预报箱数,原箱数快照用于并发核对与安全重试。 */
|
||||
export function increaseTransferForecastCartons(transferNo: string, payload: { expectedPackageCount: number; transferItemId: string; expectedItemPackageCount: number; packageCount: number }) {
|
||||
export function increaseTransferForecastCartons(transferNo: string, payload: { expectedPackageCount: number; packageCount: number }) {
|
||||
return apiRequest<Record<string, unknown>>({ method: 'POST', url: `/merchant/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}/increase-forecast-cartons`, data: payload })
|
||||
}
|
||||
export function listRelabels(params: Record<string, unknown>) {
|
||||
@@ -151,3 +151,9 @@ export interface TransferSourceCarton { cartonMark: string; productNameZh?: stri
|
||||
export function getTransferSourceCartons(operatingWarehouseId: string, inboundCode: string) {
|
||||
return apiRequest<TransferSourceCarton[]>({method:'GET',url:'/merchant/v1/last-leg/warehouse-transfers/inventory-source-cartons',params:{operatingWarehouseId,inboundCode}})
|
||||
}
|
||||
|
||||
/** 通过客户授权入口读取申请绑定的资料,不使用平台文件地址。 */
|
||||
export function downloadRelabelDocument(relabelNo: string, fileId: string, scope: string, itemId?: string) {
|
||||
const base = '/merchant/v1/last-leg/relabel-applications/' + encodeURIComponent(relabelNo)
|
||||
return apiBlobRequest({ method: 'GET', url: scope === 'SHIPPING' ? base + '/shipping-label/' + encodeURIComponent(fileId) + '/download' : base + '/files/' + encodeURIComponent(fileId) + '/download', params: scope === 'SHIPPING' ? undefined : { scope, itemId } })
|
||||
}
|
||||
|
||||
@@ -31,6 +31,14 @@ function harness(kind = 'disposal', overrides = {}) {
|
||||
return { ...state, props, calls, events, revoked }
|
||||
}
|
||||
|
||||
test('仓储转运客户确认测量弹窗不展示箱子明细和托盘尺寸', () => {
|
||||
assert.doesNotMatch(source, /<el-table :data="measurement\.cartons\?\.filter\(carton => carton\.status === 'COUNTED'\)/)
|
||||
assert.doesNotMatch(source, /<el-table-column label="尺寸\(cm\)"/)
|
||||
assert.doesNotMatch(source, /箱数、尺寸、重量/)
|
||||
assert.match(source, /<el-table-column prop="grossWeightKg" label="毛重\(kg\)"/)
|
||||
assert.match(source, /<el-table-column prop="volumeCbm" label="体积\(m³\)"/)
|
||||
})
|
||||
|
||||
for (const kind of ['disposal', 'labeling', 'transfer']) {
|
||||
test(`${kind}: opening loads photos without confirming; only reviewed evidence can be submitted`, async () => {
|
||||
const state = harness(kind)
|
||||
|
||||
@@ -96,21 +96,15 @@ onBeforeUnmount(clear)
|
||||
<template>
|
||||
<el-dialog :model-value="modelValue" :title="title" width="760px" :close-on-click-modal="false" :close-on-press-escape="!busy" :show-close="!busy" @update:model-value="!busy && emit('update:modelValue', $event)">
|
||||
<p>单号:{{ documentNo }}</p>
|
||||
<p>{{ kind === 'transfer' ? '请核对 PDA 回传的箱数、尺寸、重量、打托数据及现场照片,确认无误后再提交。' : '请核对以下 PDA 作业照片,确认作业结果无误后再提交。' }}</p>
|
||||
<p>{{ kind === 'transfer' ? '请核对 PDA 回传的箱数、重量、打托数据及现场照片,确认无误后再提交。' : '请核对以下 PDA 作业照片,确认作业结果无误后再提交。' }}</p>
|
||||
<template v-if="kind === 'transfer' && measurement">
|
||||
<p>实际签收箱数:{{ measurement.cartons?.filter(carton => carton.status === 'COUNTED').length ?? 0 }} 箱;托盘数:{{ measurement.pallets?.length ?? 0 }} 托</p>
|
||||
<el-table :data="measurement.pallets || []" max-height="220" size="small" border empty-text="暂无 PDA 打托测量数据">
|
||||
<el-table-column label="托盘号" min-width="180"><template #default="{ row }">{{ row.palletNo || `第${row.sequenceNo || '—'}托` }}</template></el-table-column>
|
||||
<el-table-column prop="cartonCount" label="箱数" width="65" />
|
||||
<el-table-column label="尺寸(cm)" min-width="130"><template #default="{ row }">{{ row.lengthCm ?? '—' }} × {{ row.widthCm ?? '—' }} × {{ row.heightCm ?? '—' }}</template></el-table-column>
|
||||
<el-table-column prop="grossWeightKg" label="毛重(kg)" width="95" />
|
||||
<el-table-column prop="volumeCbm" label="体积(m³)" width="95" />
|
||||
</el-table>
|
||||
<el-table :data="measurement.cartons?.filter(carton => carton.status === 'COUNTED') || []" max-height="220" size="small" border empty-text="暂无已签收箱子">
|
||||
<el-table-column prop="cartonMark" label="箱唛" min-width="200" />
|
||||
<el-table-column label="尺寸(cm)" min-width="180"><template #default="{ row }">{{ row.length ?? '—' }} × {{ row.width ?? '—' }} × {{ row.height ?? '—' }}</template></el-table-column>
|
||||
<el-table-column prop="weight" label="重量(kg)" width="110" />
|
||||
</el-table>
|
||||
</template>
|
||||
<p v-if="loading">正在读取作业照片…</p>
|
||||
<el-alert v-if="error" :title="error" type="error" :closable="false" />
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch, onBeforeUnmount } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getRelabel, downloadRelabelDocument, type RelabelDetail } from '../api/warehouseServices'
|
||||
const props = defineProps<{ relabelNo: string; detail?: RelabelDetail | null; inline?: boolean }>()
|
||||
const loaded = ref<RelabelDetail | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const busy = ref(false)
|
||||
let generation = 0
|
||||
const current = computed(() => props.detail || loaded.value)
|
||||
/** 各申请分别加载,失败只影响资料区,不阻断订单详情。 */
|
||||
async function load() {
|
||||
const ticket = ++generation
|
||||
loaded.value = null; error.value = ''
|
||||
if (props.detail || !props.relabelNo) { loading.value = false; return }
|
||||
loading.value = true
|
||||
try { const result = await getRelabel(props.relabelNo); if (ticket === generation) loaded.value = result }
|
||||
catch (cause) { if (ticket === generation) error.value = cause instanceof Error ? cause.message : '贴标资料加载失败' }
|
||||
finally { if (ticket === generation) loading.value = false }
|
||||
}
|
||||
watch(() => [props.relabelNo, props.detail], () => { void load() }, { immediate: true })
|
||||
interface DocumentRow { key: string; kind: string; fileId: string; fileName: string; scope: string; itemId?: string }
|
||||
const documents = computed<DocumentRow[]>(() => {
|
||||
const result: DocumentRow[] = []
|
||||
const application = current.value?.application
|
||||
if (!application) return result
|
||||
// 旧总文件仅作为兼容回显,避免与已分类型的总文件重复展示。
|
||||
const add = (kind: string, id: string | null | undefined, name: string | null | undefined, scope: string, itemId?: string) => {
|
||||
if (id) result.push({ key: scope + ':' + (itemId || '') + ':' + id, kind, fileId: id, fileName: name || '未命名文件', scope, itemId })
|
||||
}
|
||||
add('箱唛贴标文件', application.cartonTotalFileId, application.cartonTotalFileName, 'CARTON_TOTAL')
|
||||
add('SKU 贴标文件', application.skuTotalFileId, application.skuTotalFileName, 'SKU_TOTAL')
|
||||
if (application.totalFileId !== application.cartonTotalFileId && application.totalFileId !== application.skuTotalFileId)
|
||||
add('贴标总文件', application.totalFileId, application.totalFileName, 'TOTAL')
|
||||
add('面单文件', application.shippingLabelFileId, application.shippingLabelFileName, 'SHIPPING')
|
||||
for (const item of current.value?.items || []) add('明细文件', item.relabelFileId, item.relabelFileName, 'ITEM', String(item.id))
|
||||
return result
|
||||
})
|
||||
/** 使用带身份认证的下载接口,保留文件归属校验和原始文件名。 */
|
||||
async function downloadFile(key: string) {
|
||||
const row = documents.value.find(document => document.key === key)
|
||||
if (!row || busy.value) return
|
||||
busy.value = true
|
||||
const ticket = generation
|
||||
try {
|
||||
const blob = await downloadRelabelDocument(props.relabelNo, row.fileId, row.scope, row.itemId)
|
||||
if (ticket !== generation) return
|
||||
const url = URL.createObjectURL(blob); const link = document.createElement('a')
|
||||
link.href = url; link.download = row.fileName; link.click(); window.setTimeout(() => URL.revokeObjectURL(url), 1000)
|
||||
} catch (cause) { ElMessage.error(cause instanceof Error ? cause.message : '文件读取失败,请重试') }
|
||||
finally { busy.value = false }
|
||||
}
|
||||
onBeforeUnmount(() => { ++generation })
|
||||
</script>
|
||||
<template>
|
||||
<section class="relabel-documents" :class="{ 'relabel-documents--inline': inline }" aria-label="客户上传的贴标文件">
|
||||
<h3>贴标文件</h3>
|
||||
<p v-if="loading">资料加载中…</p>
|
||||
<p v-else-if="error" role="alert">{{ error }} <el-button link type="primary" @click="load">重试</el-button></p>
|
||||
<div v-else-if="inline" class="relabel-document-links">
|
||||
<div v-for="row in documents" :key="row.key" class="relabel-document-link"><span>{{ row.kind }}</span><el-button link type="primary" :disabled="busy" @click="downloadFile(row.key)">下载</el-button></div>
|
||||
<span v-if="!documents.length">暂无上传的贴标文件</span>
|
||||
</div>
|
||||
<el-table v-else :data="documents" row-key="key" border empty-text="暂无上传的贴标文件">
|
||||
<el-table-column prop="kind" label="文件类型" min-width="150" />
|
||||
<el-table-column label="操作" width="90"><template #default="{ row }"><el-button link type="primary" :disabled="busy" @click="downloadFile(String(row.key))">下载</el-button></template></el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
</template>
|
||||
<style scoped>
|
||||
.relabel-documents { margin: 16px 0; }
|
||||
.relabel-documents h3 { margin: 0 0 12px; font-size: 14px; }
|
||||
.relabel-documents--inline { display: flex; align-items: center; gap: 12px; margin: 0; min-width: 0; }
|
||||
.relabel-documents--inline h3 { flex-shrink: 0; margin: 0; color: var(--muted); font-size: 12px; font-weight: 400; }
|
||||
.relabel-document-links { display: flex; flex-wrap: wrap; gap: 4px 12px; }
|
||||
.relabel-document-link { display: flex; align-items: center; gap: 8px; }
|
||||
</style>
|
||||
@@ -0,0 +1,29 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { createSSRApp } from 'vue'
|
||||
import { renderToString } from 'vue/server-renderer'
|
||||
import { parse } from 'vue/compiler-sfc'
|
||||
|
||||
const source = readFileSync(new URL('./ReviewRejectionNotice.vue', import.meta.url), 'utf8')
|
||||
const template = parse(source).descriptor.template.content
|
||||
const render = (status, reason) => renderToString(createSSRApp({ template, props: ['status', 'reason'] }, { status, reason }))
|
||||
|
||||
test('rejected documents display the actual reason safely', async () => {
|
||||
const html = await render('REJECTED', '箱数不符\n请核对 <SKU>')
|
||||
assert.match(html, /驳回原因/)
|
||||
assert.match(html, /箱数不符\n请核对 <SKU>/)
|
||||
assert.match(html, /role="alert"/)
|
||||
})
|
||||
|
||||
test('non-rejected documents do not show an old rejection reason', async () => {
|
||||
for (const status of ['PENDING_REVIEW', 'APPROVED', 'COMPLETED', undefined]) {
|
||||
assert.doesNotMatch(await render(status, '旧审核原因'), /旧审核原因|驳回原因/)
|
||||
}
|
||||
})
|
||||
|
||||
test('legacy rejection without a reason has an explicit fallback', async () => {
|
||||
for (const reason of [undefined, '', ' ']) {
|
||||
assert.match(await render('REJECTED', reason), /暂未记录驳回原因/)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{ status?: string; reason?: string }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="status === 'REJECTED'" class="review-rejection-notice" role="alert">
|
||||
<strong>驳回原因</strong>
|
||||
<span>{{ reason?.trim() || '暂未记录驳回原因,请联系平台确认' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.review-rejection-notice{display:flex;align-items:flex-start;gap:12px;margin:12px 16px;padding:10px 12px;border:1px solid var(--el-color-danger-light-7);border-radius:4px;background:var(--el-color-danger-light-9);color:var(--el-color-danger);font-size:13px;line-height:1.7}
|
||||
.review-rejection-notice strong{flex-shrink:0;font-weight:600}
|
||||
.review-rejection-notice span{min-width:0;white-space:pre-wrap;overflow-wrap:anywhere}
|
||||
</style>
|
||||
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { getTransferPickupProgress, downloadTransferPickupEvidence, type PickupProgress } from '../api/transferPickups'
|
||||
|
||||
const props = defineProps<{ transferNo: string }>()
|
||||
const progress = ref<PickupProgress | null>(null), error = ref(''), loading = ref(false)
|
||||
const labels: Record<string, string> = { PENDING_REVIEW: '待审核', WAITING: '待安排取货', ASSIGNED: '已安排取货', PICKED: '已取货', ARRIVED: '已送仓' }
|
||||
const time = (value: string | null) => value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '—'
|
||||
let sequence = 0
|
||||
/** 业务号变化后忽略旧响应,客户之间不复用取货数据。 */
|
||||
async function load() {
|
||||
const current = ++sequence; progress.value = null; error.value = ''; loading.value = true
|
||||
try { const value = await getTransferPickupProgress(props.transferNo); if (current === sequence) progress.value = value }
|
||||
catch (cause) { if (current === sequence) error.value = cause instanceof Error ? cause.message : '取货进度加载失败' }
|
||||
finally { if (current === sequence) loading.value = false }
|
||||
}
|
||||
/** 凭证读取必须经过原单鉴权。 */
|
||||
async function download(file: { fileId: string; fileName: string }) {
|
||||
try { const blob = await downloadTransferPickupEvidence(props.transferNo, file.fileId); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = file.fileName; link.click(); setTimeout(() => URL.revokeObjectURL(url), 1000) }
|
||||
catch (cause) { error.value = cause instanceof Error ? cause.message : '取货凭证下载失败' }
|
||||
}
|
||||
watch(() => props.transferNo, load, { immediate: true }); onBeforeUnmount(() => { sequence++ })
|
||||
</script>
|
||||
<template>
|
||||
<section v-if="loading || progress || error" v-loading="loading" class="pickup-progress" aria-label="取货安排">
|
||||
<div class="pickup-heading"><h3>取货安排</h3><el-button size="small" :disabled="loading" @click="load">刷新进度</el-button></div>
|
||||
<el-alert v-if="error" :title="error" type="error" :closable="false" />
|
||||
<dl v-if="progress" class="pickup-fields">
|
||||
<div><dt>取货进度</dt><dd>{{ labels[progress.status] || progress.status }}</dd></div><div><dt>车次号</dt><dd>{{ progress.tripNo || '待安排' }}</dd></div>
|
||||
<div><dt>车牌</dt><dd>{{ progress.vehiclePlate || '待分配' }}</dd></div><div><dt>司机</dt><dd>{{ progress.driverName || '待分配' }}</dd></div>
|
||||
<div><dt>司机电话</dt><dd>{{ progress.driverPhone || '—' }}</dd></div><div><dt>计划取货发车</dt><dd>{{ time(progress.plannedDepartureAt) }}</dd></div>
|
||||
<div><dt>实提箱数</dt><dd>{{ progress.actualPackages ?? '待取货' }}</dd></div><div><dt>取货时间</dt><dd>{{ time(progress.pickedAt) }}</dd></div>
|
||||
<div><dt>送仓时间</dt><dd>{{ time(progress.arrivedAt) }}</dd></div><div v-if="progress.remark"><dt>取货备注</dt><dd>{{ progress.remark }}</dd></div>
|
||||
</dl>
|
||||
<div v-if="progress?.evidence.length" class="pickup-files"><span>取货凭证</span><el-button v-for="file in progress.evidence" :key="file.fileId" link type="primary" @click="download(file)">{{ file.fileName }}</el-button></div>
|
||||
</section>
|
||||
</template>
|
||||
<style scoped>.pickup-progress{padding:16px;border:1px solid var(--border,#e5e7eb);border-radius:6px;margin:16px 0}.pickup-heading{display:flex;justify-content:space-between;align-items:center}.pickup-heading h3{margin:0;font-size:15px}.pickup-fields{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px 20px;margin:16px 0 0}.pickup-fields>div{display:flex;gap:10px}.pickup-fields dt{color:var(--muted,#64748b);flex-shrink:0}.pickup-fields dd{margin:0;overflow-wrap:anywhere}.pickup-files{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-top:14px}</style>
|
||||
@@ -0,0 +1,47 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import ts from 'typescript'
|
||||
const source = readFileSync(new URL('./transferCartonPrinting.ts', import.meta.url), 'utf8')
|
||||
const js = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS } }).outputText
|
||||
const exports = {}
|
||||
new Function('exports', js)(exports)
|
||||
const prepare = exports.prepareTransferCartonLabels
|
||||
|
||||
test('prints forecast counts independently of quantities or order fields and escapes cargo', () => {
|
||||
const rows = [{ sku: 'A<script>', name: '货物 & 品名', expectedPackageCount: 12 }]
|
||||
let next = 0
|
||||
const html = prepare(rows, () => `BX-${++next}`)
|
||||
assert.equal((html.match(/<section /g) || []).length, 12)
|
||||
assert.match(html, /12 \/ 12/)
|
||||
assert.match(html, /A<script>/)
|
||||
assert.equal(rows[0].generatedCartonMarks.length, 12)
|
||||
assert.equal(prepare(rows, () => { throw new Error('must retain labels') }), html)
|
||||
})
|
||||
|
||||
test('adding boxes or rows keeps previously printed identities', () => {
|
||||
const rows = [{ sku: 'A', name: '', expectedPackageCount: 2 }]
|
||||
let next = 0
|
||||
prepare(rows, () => `BX-${++next}`)
|
||||
rows[0].expectedPackageCount = 3
|
||||
rows.push({ sku: 'B', name: '', expectedPackageCount: 1 })
|
||||
const html = prepare(rows, () => `BX-${++next}`)
|
||||
assert.deepEqual(rows[0].generatedCartonMarks, ['BX-1', 'BX-2', 'BX-3'])
|
||||
assert.match(html, /BX-4/)
|
||||
})
|
||||
|
||||
test('invalid or excessive forecasts do not generate any labels', () => {
|
||||
for (const count of [0, -1, 1.5, undefined, 5001]) {
|
||||
const row = { sku: '', name: '', expectedPackageCount: count }
|
||||
assert.throws(() => prepare([row]), /预计箱数/)
|
||||
assert.equal(row.generatedCartonMarks, undefined)
|
||||
}
|
||||
})
|
||||
|
||||
test('print button cannot create, update or submit an order', () => {
|
||||
const view = readFileSync(new URL('../views/WarehouseServiceView.vue', import.meta.url), 'utf8')
|
||||
const handler = view.slice(view.indexOf('function printForecastCartons()'), view.indexOf('async function submit('))
|
||||
assert.match(handler, /prepareTransferCartonLabels\(cargoRows.value\)/)
|
||||
assert.doesNotMatch(handler, /createTransfer|updateTransfer|submitTransfer|getTransferCartonLabel|form\./)
|
||||
assert.match(view, /cartonMarks: item.generatedCartonMarks/)
|
||||
})
|
||||
@@ -0,0 +1,27 @@
|
||||
export interface PrintableCargoRow {
|
||||
sku: string
|
||||
name: string
|
||||
expectedPackageCount?: number
|
||||
generatedCartonMarks?: string[]
|
||||
}
|
||||
|
||||
const escapeHtml = (value: string) => value.replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[char]!)
|
||||
|
||||
/** Generate labels in memory only. The later order request carries these same identities. */
|
||||
export function prepareTransferCartonLabels(rows: PrintableCargoRow[], newMark: () => string = () => {
|
||||
const bytes = crypto.getRandomValues(new Uint8Array(16))
|
||||
return `BX-${Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('').toUpperCase()}`
|
||||
}) {
|
||||
const total = rows.reduce((sum, row) => sum + Number(row.expectedPackageCount || 0), 0)
|
||||
if (!rows.length || rows.some(row => !Number.isSafeInteger(row.expectedPackageCount) || Number(row.expectedPackageCount) < 1) || total > 5000) {
|
||||
throw new Error('请填写货物的预计箱数(正整数),单次最多打印 5000 箱')
|
||||
}
|
||||
let index = 0
|
||||
const labels = rows.flatMap(row => {
|
||||
const count = Number(row.expectedPackageCount)
|
||||
const marks = row.generatedCartonMarks ||= []
|
||||
while (marks.length < count) marks.push(newMark())
|
||||
return marks.slice(0, count).map(mark => `<section class="label"><div class="title">仓储转运箱唛</div><div class="mark">${escapeHtml(mark)}</div><div>${++index} / ${total}</div><div>SKU:${escapeHtml(row.sku)}</div><div>${escapeHtml(row.name)}</div></section>`)
|
||||
})
|
||||
return `<!doctype html><html><head><meta charset="utf-8"><title>仓储转运箱唛</title><style>@page{size:100mm 100mm;margin:0}body{margin:0;font-family:Arial,sans-serif}.label{box-sizing:border-box;width:100mm;height:100mm;padding:10mm;break-after:page;display:flex;flex-direction:column;justify-content:center;gap:6mm;text-align:center;overflow-wrap:anywhere}.label:last-child{break-after:auto}.title{font-size:22px;font-weight:bold}.mark{font-size:23px;font-weight:bold}</style></head><body>${labels.join('')}</body></html>`
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { useAuthStore } from '@/modules/auth/public'
|
||||
import { getMerchantWarehouses, type MerchantWarehouse } from '@/modules/first-leg/public'
|
||||
import { createIdempotencyKey } from '@/utils/idempotencyKey'
|
||||
import { createReturnOrder, downloadReturnPhoto, getReturnOrder, listReturnOrders, updateReturnOrder, type ReturnOrderDetail, type ReturnOrderSummary, type ReturnReceipt } from '../api/returnOrders'
|
||||
import ReviewRejectionNotice from '../components/ReviewRejectionNotice.vue'
|
||||
import { getReturnInventoryLabel } from '../api/returnInventory'
|
||||
import { formatReturnVolume, receiptBoxDetails, receiptPhysicalDetails, receiptPrintableBoxCount, receiptTrackingNo } from '../utils/returnReceiptDisplay'
|
||||
|
||||
@@ -363,6 +364,7 @@ onBeforeUnmount(clearPhotoUrls)
|
||||
<div class="return-detail-title"><strong>{{ selected.order.returnNo }}</strong><span>{{ operationWarehouseLabel(selected.order.overseasWarehouseId) }} · 退货签收</span></div>
|
||||
<div class="return-detail-actions"><el-button v-if="canCreateRelabel && canApplyRelabel(selected.order)" type="primary" plain @click="openRelabel(selected.order)">贴标申请</el-button><el-tag :type="statusType(selected.order.status, selected.order.needRelabel)" :class="statusClass(selected.order.status, selected.order.needRelabel)" effect="light">{{ statusLabel(selected.order.status, selected.order.needRelabel) }}</el-tag></div>
|
||||
</header>
|
||||
<ReviewRejectionNotice :status="selected.order.status" :reason="selected.remark" />
|
||||
<section class="return-detail-section">
|
||||
<h3>签收概览</h3>
|
||||
<div class="return-detail-grid">
|
||||
@@ -374,7 +376,7 @@ onBeforeUnmount(clearPhotoUrls)
|
||||
<div><span>联系电话</span><strong>{{ selected.order.warehouseContactPhone || '—' }}</strong></div>
|
||||
<div><span>创建时间</span><strong>{{ formatDateTime(selected.order.createdAt) }}</strong></div>
|
||||
</div>
|
||||
<div v-if="selected.remark" class="return-detail-remark"><span>备注</span><strong>{{ selected.remark }}</strong></div>
|
||||
<div v-if="selected.remark && selected.order.status !== 'REJECTED'" class="return-detail-remark"><span>备注</span><strong>{{ selected.remark }}</strong></div>
|
||||
</section>
|
||||
<section v-if="selected.order.relabelApplications?.length" class="return-detail-section">
|
||||
<h3>换标子单({{ selected.order.relabelApplications?.length || 0 }})</h3>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import TransferPickupProgressPanel from '../components/TransferPickupProgressPanel.vue'
|
||||
import { computed, nextTick, onActivated, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
@@ -7,7 +8,9 @@ import QueryToolbar, { type QueryField, type QueryModel } from '@/components/com
|
||||
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 {
|
||||
createDiscard, createRelabel, createTransfer, getDiscard, getRelabel, getTransfer, resubmitRelabel,
|
||||
@@ -28,13 +31,14 @@ import {
|
||||
} 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 { 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 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[] }
|
||||
@@ -133,6 +137,7 @@ 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
|
||||
@@ -149,29 +154,34 @@ interface ServiceDetail extends Record<string, unknown> {
|
||||
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({ itemId: '', additionalCount: 1 })
|
||||
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.itemId = String(selectedDetail.value.items?.[0]?.id || '')
|
||||
increaseForecastForm.additionalCount = 1; increaseForecastOpen.value = true
|
||||
}
|
||||
/** 只补箱唛,SKU预报件数不随包装箱数自动改变。 */
|
||||
async function saveIncreaseForecast() {
|
||||
if (increasingForecast.value || !forecastSnapshot.value?.transferNo) return
|
||||
const snapshot = forecastSnapshot.value
|
||||
const item = snapshot.items?.find(row => String(row.id) === increaseForecastForm.itemId)
|
||||
if (!item || !Number.isSafeInteger(increaseForecastForm.additionalCount) || increaseForecastForm.additionalCount < 1) { ElMessage.warning('请选择SKU并填写新增箱数'); return }
|
||||
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), transferItemId: String(item.id), expectedItemPackageCount: Number(item.packageCount), packageCount: Number(item.packageCount) + increaseForecastForm.additionalCount })
|
||||
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 : '增加预报箱数失败') }
|
||||
@@ -324,6 +334,9 @@ const relabelCandidatesLoading = ref(false)
|
||||
const transferInventoryCandidates = ref<RelabelInventoryCandidate[]>([])
|
||||
const selectedTransferInventory = ref('')
|
||||
const inventoryTransfer = computed(() => props.service === 'transfer' && Boolean(selectedTransferInventory.value))
|
||||
// 来源库存发往FBA时默认贴标,私人仓和外来货保留原有选择。
|
||||
const transferRelabelRequired = computed(() => inventoryTransfer.value && transferDestinationType.value === 'FBA')
|
||||
watch(transferRelabelRequired, (required) => { if (required) form.relabel = true }, { flush: 'sync' })
|
||||
const transferSourceCartons = ref<TransferSourceCarton[]>([])
|
||||
const transferSourceLoading = ref(false)
|
||||
const transferSourceError = ref('')
|
||||
@@ -338,6 +351,8 @@ async function loadTransferSourceCartons() {
|
||||
if (selectedTransferInventory.value.startsWith('RETURN_ORDER:')) {
|
||||
const returnNo = selectedTransferInventory.value.slice('RETURN_ORDER:'.length)
|
||||
transferSourceCartons.value = transferInventoryCandidates.value
|
||||
.filter(item => !['COMPLETED', 'PARTIALLY_COMPLETED'].includes(item.relabelStatus || '')
|
||||
&& Number(item.relabelledQuantity || item.relabelledPackages || 0) === 0)
|
||||
.filter(item => (item.sourceType === 'RETURN_CARTON' || item.sourceType === 'RETURN_PARCEL')
|
||||
&& String(item.inboundOrderNo || '').trim() === returnNo && Number(item.cartonAvailablePackages || 0) > 0)
|
||||
.filter(item => Boolean(item.cartonMark))
|
||||
@@ -582,6 +597,7 @@ async function openEdit(transferNo: string) {
|
||||
...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) {
|
||||
@@ -1150,7 +1166,20 @@ 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 (transferRelabelRequired.value) form.relabel = true
|
||||
const primaryWarehouse = props.service === 'relabel' ? form.sourceWarehouse : form.warehouse
|
||||
const relabelRows = labelingMode.value === 'carton' ? cartonDetailRows.value : skuRelabelRows.value
|
||||
const invalidCargo = props.service === 'disposal'
|
||||
@@ -1272,7 +1301,7 @@ async function submit(action: 'draft' | 'submit' = 'submit') {
|
||||
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 })), 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 }
|
||||
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
|
||||
@@ -1450,7 +1479,7 @@ async function loadTransferInventoryCandidates(warehouseId: string) {
|
||||
relabelCandidatesLoading.value = true
|
||||
try {
|
||||
// 共用接口包含退货历史及占用中的箱子,转运只展示当前可整箱使用的来源。
|
||||
transferInventoryCandidates.value = (await listRelabelInventoryCandidates(warehouseId)).filter(item =>
|
||||
transferInventoryCandidates.value = (await listRelabelInventoryCandidates(warehouseId)).filter(isUnlabelledTransferCandidate).filter(item =>
|
||||
item.sourceType === 'RETURN_CARTON' || item.sourceType === 'RETURN_PARCEL'
|
||||
? Number(item.cartonAvailablePackages || 0) > 0
|
||||
: Number(item.availablePackages || 0) > 0)
|
||||
@@ -1519,6 +1548,12 @@ const transferInventoryOptions = computed(() => {
|
||||
}
|
||||
return [...grouped.values()]
|
||||
})
|
||||
/** 已完成或部分完成换标的退货实物箱不再作为原箱来源;普通来源由逐箱接口过滤。 */
|
||||
function isUnlabelledTransferCandidate(item: RelabelInventoryCandidate) {
|
||||
if (item.sourceType !== 'RETURN_CARTON' && item.sourceType !== 'RETURN_PARCEL') return true
|
||||
return !['COMPLETED', 'PARTIALLY_COMPLETED'].includes(item.relabelStatus || '')
|
||||
&& inventoryQuantity(item.relabelledQuantity || item.relabelledPackages) === 0
|
||||
}
|
||||
async function selectTransferInventory(value: string) {
|
||||
selectedTransferInventory.value = value || ''
|
||||
selectedTransferSourceOrderIds.value = []
|
||||
@@ -1707,7 +1742,7 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
|
||||
<label v-if="props.service === 'labeling'"><span>贴标要求</span><el-input v-model="form.requirement" placeholder="请输入贴标要求" /></label>
|
||||
<label v-else-if="props.service === 'relabel' || (props.service === 'transfer' && form.deliveryMethod === 'CUSTOMER_DELIVERY')"><span>物流跟踪号</span><el-input v-model="form.trackingNo" placeholder="请输入物流跟踪号(选填)" /></label>
|
||||
<label class="warehouse-span-2"><span>备注</span><el-input v-model="form.remark" type="textarea" :rows="2" placeholder="选填" /></label>
|
||||
<div v-if="props.service === 'transfer'" class="warehouse-choice-field warehouse-choice-field--relabel warehouse-span-2"><span><i>*</i>是否贴标</span><el-radio-group v-model="form.relabel"><el-radio-button :value="false">不需要贴标</el-radio-button><el-radio-button :value="true">需要贴标</el-radio-button></el-radio-group><small>{{ form.relabel ? '已开启贴标,请在右侧填写贴标资料' : 'FBA 仓和私人仓均可选择是否贴标' }}</small></div>
|
||||
<div v-if="props.service === 'transfer'" class="warehouse-choice-field warehouse-choice-field--relabel warehouse-span-2"><span><i>*</i>是否贴标</span><el-radio-group v-model="form.relabel"><el-radio-button v-if="!transferRelabelRequired" :value="false">不需要贴标</el-radio-button><el-radio-button :value="true">需要贴标</el-radio-button></el-radio-group><small>{{ form.relabel ? '已开启贴标,请在右侧填写贴标资料' : 'FBA 仓和私人仓均可选择是否贴标' }}</small></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="warehouse-card warehouse-form-section warehouse-transfer-cargo-card">
|
||||
@@ -1782,11 +1817,11 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
|
||||
<el-alert v-if="transferSourceError" :title="transferSourceError" type="error" :closable="false"><el-button link @click="loadTransferSourceCartons">重新加载</el-button></el-alert>
|
||||
<div class="warehouse-table-scroll"><table class="warehouse-edit-table"><thead><tr><th>箱号</th><th>商品 SKU</th><th>中文品名</th><th>箱内数量(件)</th><th>原目的仓</th></tr></thead><tbody>
|
||||
<tr v-for="box in transferSourceCartons" :key="box.cartonMark"><td>{{ box.cartonMark }}</td><td>{{ box.items.map(item => item.skuCode || '—').join(' / ') }}</td><td>{{ box.productNameZh || '—' }}</td><td>{{ box.quantity ?? '未记录' }}</td><td>{{ [box.destinationWarehouseCode, box.destinationWarehouseName].filter(Boolean).join(' · ') || '未指定' }}</td></tr>
|
||||
<tr v-if="!transferSourceLoading && !transferSourceCartons.length"><td colspan="5"><div class="warehouse-empty">{{ transferSourceError ? '加载失败,请重试' : '原库存单据未记录逐箱明细,不虚构箱号;按原库存汇总转运' }}</div></td></tr>
|
||||
<tr v-if="!transferSourceLoading && !transferSourceCartons.length"><td colspan="5"><div class="warehouse-empty">{{ transferSourceError ? '加载失败,请重试' : '当前来源单暂无未换标的箱明细' }}</div></td></tr>
|
||||
</tbody></table></div>
|
||||
<div v-if="!transferSourceLoading && !transferSourceError && !transferSourceCartons.length" class="warehouse-table-scroll"><table class="warehouse-edit-table"><thead><tr><th>商品 SKU</th><th>中文品名</th><th>库存箱数</th><th>原目的仓</th></tr></thead><tbody><tr v-for="row in cargoRows" :key="row.id"><td>{{ row.sku || '—' }}</td><td>{{ row.name || '—' }}</td><td>{{ row.expectedPackageCount }}</td><td>{{ transferInventoryCandidates.filter(item => item.inboundCode === selectedTransferInventory && item.skuCode === row.sku).map(item => item.destinationFbaWarehouseCode || item.destinationFbaWarehouseName).filter(Boolean).join(' / ') || '未指定' }}</td></tr></tbody></table></div>
|
||||
</section>
|
||||
<div v-if="!inventoryTransfer && (props.service !== 'disposal' || !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 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' || !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' || !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="props.service === 'transfer' ? '选填' : '请输入商品 SKU'" /></td><td><el-input v-model="row.name" :readonly="props.service === 'disposal'" :placeholder="props.service === 'transfer' ? '选填' : '请输入中文品名'" /></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="请输入箱数" /></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">{{ props.service === 'disposal' ? '请先选择入库单,系统将自动带出 SKU 和可用箱数' : '暂无货物明细,请点击“添加行”' }}</div></td></tr></tbody></table></div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -1826,8 +1861,9 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
|
||||
<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>
|
||||
<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 ? relabelSkuCount(selectedRelabelDetail.items) : '—' }}</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>{{ 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>预计箱数</span><strong>{{ selectedDetail?.packageCount ?? '—' }}</strong></div><div><span>实际签收箱数</span><strong>{{ selectedDetail?.cartons?.filter((carton) => carton.status === 'COUNTED').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 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>
|
||||
<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>{{ 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>{{ 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>{{ selectedDetail?.cartons?.filter((carton) => carton.status === 'COUNTED').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 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">
|
||||
<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></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 ?? '—' }}</td><td>{{ item.cartonMarks?.join('、') || '全部库存(历史记录无箱唛快照)' }}</td></tr></tbody></table></div>
|
||||
<h3>废弃现场照片</h3><div v-if="selectedDetail?.photos?.length" 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><div v-else class="warehouse-empty">暂无已提交的废弃现场照片</div>
|
||||
@@ -1836,13 +1872,13 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
|
||||
<div class="warehouse-section-heading warehouse-transfer-receipt-heading"><h3>预报箱唛({{ selectedDetail?.packageCount || 0 }} 箱)</h3><div class="warehouse-transfer-receipt-actions"><el-button v-if="canIncreaseForecast" type="primary" size="small" @click="openIncreaseForecast"><Plus :size="14" />增加预报箱数</el-button><el-button type="primary" size="small" @click="printTransferCartons(String(selectedDetail?.transferNo || ''))"><Printer :size="14" />打印预报箱唛</el-button><el-button size="small" @click="showForecastCartons = true">查看预报箱唛</el-button></div></div>
|
||||
<el-dialog v-model="showForecastCartons" title="预报箱唛明细" width="900px" align-center destroy-on-close append-to-body><el-table :data="selectedDetail?.cartons || []" size="small" border><el-table-column prop="cartonMark" label="仓库唛头" min-width="300" /><el-table-column label="预报SKU" min-width="240"><template #default="{ row }">{{ selectedDetail?.items?.find(item => String(item.id) === String(row.transferItemId))?.skuCode || '—' }}</template></el-table-column><el-table-column label="清点状态" width="130"><template #default="{ row }">{{ row.status === 'COUNTED' ? '已清点' : row.status === 'NOT_RECEIVED' ? '未收到' : '待清点' }}</template></el-table-column></el-table><template #footer><el-button @click="showForecastCartons = false">关闭</el-button></template></el-dialog>
|
||||
<el-dialog v-model="increaseForecastOpen" title="增加预报箱数" width="560px" align-center destroy-on-close append-to-body :close-on-click-modal="!increasingForecast" :show-close="!increasingForecast">
|
||||
<el-form label-width="110px"><el-form-item label="预报SKU"><el-select v-model="increaseForecastForm.itemId" :disabled="increasingForecast" style="width:100%"><el-option v-for="item in forecastSnapshot?.items || []" :key="String(item.id)" :value="String(item.id)" :label="item.skuCode || item.productNameZh || '未命名SKU'" /></el-select></el-form-item><el-form-item label="新增箱数"><el-input-number v-model="increaseForecastForm.additionalCount" placeholder="请输入新增箱数" :disabled="increasingForecast" :min="1" :max="Math.max(1, 5000 - Number(forecastSnapshot?.packageCount || 0))" :precision="0" /></el-form-item></el-form><p class="warehouse-form-hint">保留原箱唛,仅补生成新增箱唛;SKU预报件数保持不变。</p>
|
||||
<el-form label-width="110px"><el-form-item label="新增箱数"><el-input-number v-model="increaseForecastForm.additionalCount" placeholder="请输入新增箱数" :disabled="increasingForecast" :min="1" :max="Math.max(1, 5000 - Number(forecastSnapshot?.packageCount || 0))" :precision="0" /></el-form-item></el-form><p class="warehouse-form-hint">保留原箱唛,仅补生成新增箱唛;箱内SKU和件数以PDA清点为准,SKU预报总件数保持不变。</p>
|
||||
<template #footer><el-button :disabled="increasingForecast" @click="increaseForecastOpen = false">取消</el-button><el-button type="primary" :loading="increasingForecast" @click="saveIncreaseForecast">确认增加</el-button></template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
<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>
|
||||
<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>PDA 打托实测数据</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">
|
||||
@@ -1887,16 +1923,18 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
|
||||
<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' && canManageRelabelFiles" class="warehouse-card warehouse-form-section">
|
||||
<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 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>
|
||||
<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-hint">需要补充或替换资料时,可在对应明细行上传 SKU 或箱唛文件。</div>
|
||||
@@ -2132,10 +2170,14 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
|
||||
.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-transfer-received-card .warehouse-transfer-receipt-heading{justify-content:flex-start;flex-wrap:wrap;gap:10px 16px}
|
||||
.warehouse-service-page--detail .warehouse-transfer-receipt-heading h3{flex:0 0 auto;margin:0;padding:0;border:0}
|
||||
.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-detail-header+.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 14px 10px}
|
||||
.warehouse-service-page--detail>.warehouse-detail-header+.warehouse-card .warehouse-transfer-overview-grid>div{min-height:24px;gap:8px;padding:1px 0}
|
||||
.warehouse-service-page--detail>.warehouse-detail-header+.warehouse-card .warehouse-transfer-overview-grid>div>span{flex:0 0 84px;min-width: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>
|
||||
|
||||
@@ -2,6 +2,7 @@ import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { transpileModule, ScriptTarget } from 'typescript'
|
||||
import { computed, ref, reactive, watch } from 'vue'
|
||||
|
||||
const view = readFileSync(new URL('./WarehouseServiceView.vue', import.meta.url), 'utf8')
|
||||
|
||||
@@ -18,6 +19,8 @@ test('退货箱明细与来源下拉使用同一份逐箱库存,不请求普
|
||||
cartonSkuItems: [{ skuCode: 'A', quantity: 1 }] },
|
||||
{ sourceType: 'RETURN_CARTON', inboundOrderNo: 'OTHER', cartonMark: 'FOREIGN', cartonAvailablePackages: 1 },
|
||||
{ sourceType: 'RETURN_CARTON', inboundOrderNo: 'RET-1', cartonMark: 'OCCUPIED', cartonAvailablePackages: 0 },
|
||||
{ sourceType: 'RETURN_CARTON', inboundOrderNo: 'RET-1', cartonMark: 'LABELLED', cartonAvailablePackages: 1, relabelStatus: 'COMPLETED' },
|
||||
{ sourceType: 'RETURN_CARTON', inboundOrderNo: 'RET-1', cartonMark: 'PARTIAL', cartonAvailablePackages: 1, relabelledQuantity: 2 },
|
||||
] }, getTransferSourceCartons: () => { throw Error('不应调用普通来源箱查询') } }
|
||||
const load = new Function(...Object.keys(ctx), 'let transferSourceRequest=0;' + code + ';return loadTransferSourceCartons')(...Object.values(ctx))
|
||||
await load()
|
||||
@@ -28,6 +31,27 @@ test('退货箱明细与来源下拉使用同一份逐箱库存,不请求普
|
||||
assert.equal(ctx.transferSourceLoading.value, false)
|
||||
})
|
||||
|
||||
test('来源库存发往FBA默认开启贴标,清除来源或切换私人仓后恢复选项', () => {
|
||||
const source = ref(''), destination = ref('NONE'), form = reactive({ relabel: false })
|
||||
const inventoryTransfer = computed(() => Boolean(source.value))
|
||||
const body = view.slice(view.indexOf('const transferRelabelRequired ='), view.indexOf('const transferSourceCartons ='))
|
||||
const required = new Function('computed', 'watch', 'inventoryTransfer', 'transferDestinationType', 'form', body + ';return transferRelabelRequired')(
|
||||
computed, watch, inventoryTransfer, destination, form)
|
||||
destination.value = 'FBA'
|
||||
assert.equal(form.relabel, false)
|
||||
source.value = 'IN-1'
|
||||
assert.equal(required.value, true)
|
||||
assert.equal(form.relabel, true)
|
||||
destination.value = 'PRIVATE'
|
||||
assert.equal(required.value, false)
|
||||
form.relabel = false
|
||||
destination.value = 'FBA'
|
||||
assert.equal(form.relabel, true)
|
||||
source.value = ''
|
||||
assert.equal(required.value, false)
|
||||
assert.match(view, /<el-radio-button v-if="!transferRelabelRequired" :value="false">不需要贴标/)
|
||||
})
|
||||
|
||||
function setup() {
|
||||
const body = view.slice(view.indexOf('async function selectTransferInventory('), view.indexOf('function selectTransferInboundMethod('))
|
||||
const code = transpileModule(body, { compilerOptions: { target: ScriptTarget.ES2022 } }).outputText
|
||||
|
||||
@@ -230,7 +230,7 @@ test('进入复核后客户订单回显换标完成状态', () => {
|
||||
|
||||
test('审核驳回后展示原因并允许替换资料后重新提交', () => {
|
||||
assert.match(api, /export function resubmitRelabel\(/)
|
||||
assert.match(view, /贴标单审核已驳回/)
|
||||
assert.match(view, /<ReviewRejectionNotice/)
|
||||
assert.match(view, /application\.reviewRemark/)
|
||||
assert.match(view, /修改后重新提交/)
|
||||
assert.match(view, /await resubmitRelabel\(/)
|
||||
|
||||
@@ -102,10 +102,12 @@ test('仓储转运不要求客户填写内部入仓号和我方箱唛,但换
|
||||
assert.doesNotMatch(view, /<th v-if="props\.service === 'transfer'">FNSKU \/ 箱唛<\/th>/)
|
||||
})
|
||||
|
||||
test('仓储转运提交后统一打印服务端预报箱唛', () => {
|
||||
test('仓储转运填写货物即可打印,保存或提交保留已打印箱号', () => {
|
||||
assert.doesNotMatch(view, /printTransferDraftCartonMarks|CT-\$\{batch\}/)
|
||||
assert.match(view, /getTransferCartonLabel\(transferNo\)/)
|
||||
assert.match(view, /打印预报箱唛/)
|
||||
assert.match(view, /@click="printForecastCartons"/)
|
||||
assert.match(view, /cartonMarks: item.generatedCartonMarks/)
|
||||
})
|
||||
|
||||
test('仓储转运换标资料按步骤分组展示', () => {
|
||||
@@ -404,5 +406,5 @@ test('预报实收同区对照、托盘卡片换行、签收按钮紧邻标题',
|
||||
const receipt = view.slice(view.indexOf('warehouse-transfer-received-card', start)).split('<el-dialog')[0]
|
||||
assert.ok(view.includes('打印预报箱唛')); assert.ok(view.includes('增加预报箱数'))
|
||||
assert.ok(receipt.includes('查看箱号明细'))
|
||||
assert.match(view, /warehouse-transfer-receipt-heading h3\{flex:0 0 auto/)
|
||||
assert.match(view, /warehouse-section-heading h3\{flex:0 0 auto/)
|
||||
})
|
||||
|
||||
@@ -52,13 +52,13 @@ test('keeps standalone business pages at the client sidebar root', () => {
|
||||
{ title: '首页', children: undefined },
|
||||
{
|
||||
title: '订单中心',
|
||||
children: ['头程订单', '入库订单', '派送订单', '海外采购', '美国本土卡', '美本土散板'],
|
||||
children: ['头程订单', '入库订单', '派送订单'],
|
||||
},
|
||||
{ title: '报关资料', children: undefined },
|
||||
{ title: '船期价格', children: undefined },
|
||||
{
|
||||
title: '海外仓',
|
||||
children: ['贴标申请', '仓储转运', '货物废弃', '退货换标'],
|
||||
children: ['贴标申请', '仓储转运', '货物废弃', '退货换标', '海外采购', '美国本土卡', '美本土散板'],
|
||||
},
|
||||
{
|
||||
title: '费用与账单',
|
||||
@@ -90,15 +90,15 @@ test('flattens visible leaves with unique paths', () => {
|
||||
'/orders/first-leg',
|
||||
'/orders/inbound',
|
||||
'/orders/delivery',
|
||||
'/cross-border/procurement',
|
||||
'/cross-border/domestic-trucking',
|
||||
'/cross-border/pallet-shipping',
|
||||
'/first-leg/customs-documents',
|
||||
'/first-leg/sailing-prices',
|
||||
'/overseas-warehouse/labeling',
|
||||
'/overseas-warehouse/storage-transfer',
|
||||
'/overseas-warehouse/cargo-disposal',
|
||||
'/overseas-warehouse/return-relabel',
|
||||
'/cross-border/procurement',
|
||||
'/cross-border/domestic-trucking',
|
||||
'/cross-border/pallet-shipping',
|
||||
'/finance/account',
|
||||
'/finance/bills',
|
||||
'/company/profile',
|
||||
|
||||
@@ -54,9 +54,6 @@ export const clientNavigation: ClientNavigationNode[] = [
|
||||
requireLeaf('first-leg-orders', '头程订单'),
|
||||
requireLeaf('orders-inbound', '入库订单'),
|
||||
requireLeaf('orders-delivery', '派送订单'),
|
||||
requireLeaf('cross-border-procurement'),
|
||||
requireLeaf('cross-border-domestic-trucking'),
|
||||
requireLeaf('cross-border-pallet-shipping'),
|
||||
],
|
||||
},
|
||||
requireLeaf('first-leg-customs-documents', '报关资料'),
|
||||
@@ -70,6 +67,9 @@ export const clientNavigation: ClientNavigationNode[] = [
|
||||
requireLeaf('overseas-storage-transfer'),
|
||||
requireLeaf('overseas-cargo-disposal'),
|
||||
requireLeaf('overseas-return-relabel', '退货换标'),
|
||||
requireLeaf('cross-border-procurement'),
|
||||
requireLeaf('cross-border-domestic-trucking'),
|
||||
requireLeaf('cross-border-pallet-shipping'),
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user