From a0e46a05e535524cd52accff3c101346ae00cc98 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Sun, 13 Sep 2026 21:10:05 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../overseas-warehouse/api/dispatches.ts | 16 +- .../overseas-warehouse/api/lastMileOrders.ts | 3 +- .../api/warehouseTransfers.test.ts | 28 + .../api/warehouseTransfers.ts | 117 +++ .../components/SmartDispatchDialog.vue | 793 +++++++++++++++--- .../WarehouseTransferReviewDialog.vue | 77 ++ .../lastMilePresentation.ts | 4 +- .../layoutAcceptance.test.ts | 4 +- .../pages/DispatchesView.test.ts | 16 +- .../pages/DispatchesView.vue | 550 ++++++++++-- .../pages/LastMileOrderDetailView.vue | 1 - .../pages/LastMileOrdersView.behavior.test.ts | 2 +- .../pages/LastMileOrdersView.vue | 20 +- .../pages/ReturnInventoryManagementView.vue | 21 +- .../pages/prototypeContract.test.ts | 18 +- src/modules/overseas-warehouse/permissions.ts | 1 + src/modules/overseas-warehouse/routes.test.ts | 3 +- src/modules/overseas-warehouse/types.ts | 4 +- 18 files changed, 1451 insertions(+), 227 deletions(-) create mode 100644 src/modules/overseas-warehouse/api/warehouseTransfers.test.ts create mode 100644 src/modules/overseas-warehouse/api/warehouseTransfers.ts create mode 100644 src/modules/overseas-warehouse/components/WarehouseTransferReviewDialog.vue diff --git a/src/modules/overseas-warehouse/api/dispatches.ts b/src/modules/overseas-warehouse/api/dispatches.ts index 75382404..229aa123 100644 --- a/src/modules/overseas-warehouse/api/dispatches.ts +++ b/src/modules/overseas-warehouse/api/dispatches.ts @@ -12,8 +12,8 @@ export interface DispatchQuery { } export interface DispatchPalletCandidateDto { - planId: string - planPalletId: string + planId?: string | null + planPalletId?: string | null warehousePalletId?: string | null orderId: string customerId?: string | null @@ -36,8 +36,9 @@ export interface DispatchPalletCandidateDto { fbaWarehouseCode?: string | null packageCount?: number | null volumeCbm?: number | null - sourceStage?: 'PLAN' | 'RECEIVED' | null + sourceStage?: 'PLAN' | 'RECEIVED' | 'RELABEL' | null cartonMarks?: string | null + relabelCartonMarks?: string | null } export interface DispatchCandidateQuery { @@ -57,6 +58,7 @@ export interface PreArrangeDispatchPayload { fbaAppointmentStatus?: string vehicleAppointmentStatus?: string draftToken?: string + relabelNo?: string } export interface DispatchSummaryDto { @@ -147,6 +149,13 @@ export async function cancelDispatch(dispatchNo: string) { return unwrapApiResponse(response) } +export async function removeDispatchItem(dispatchNo: string, itemId: string) { + const response = await overseasHttp.delete, ApiEnvelope>( + `/platform/v1/last-leg/dispatches/${encodeURIComponent(dispatchNo)}/items/${encodeURIComponent(itemId)}`, + ) + return unwrapApiResponse(response) +} + export async function listDispatchCandidates(query: DispatchCandidateQuery) { const response = await overseasHttp.get, ApiEnvelope>( '/platform/v1/last-leg/dispatches/candidates', @@ -219,7 +228,6 @@ export interface TransferDeliveryPayload { paidAt?: string; paymentMethod?: 'ONLINE' | 'BANK_TRANSFER' | 'CASH' | 'OTHER'; paymentReferenceNo?: string paymentVoucherFileIds?: string[]; chargeableQuantity?: number; quantityUnit?: string; unitPrice?: number periodStart?: string; periodEnd?: string; remark?: string }> - palletCartonMarks?: Record } export async function transferDispatchToDelivery(dispatchNo: string, payload: TransferDeliveryPayload) { const response = await overseasHttp.post, ApiEnvelope>( diff --git a/src/modules/overseas-warehouse/api/lastMileOrders.ts b/src/modules/overseas-warehouse/api/lastMileOrders.ts index f6f74abe..8b5cf6bf 100644 --- a/src/modules/overseas-warehouse/api/lastMileOrders.ts +++ b/src/modules/overseas-warehouse/api/lastMileOrders.ts @@ -25,6 +25,7 @@ export interface LastMileOrderQuery { page: number size: number orderNo?: string + relabelNo?: string orderType?: string inboundCode?: string lxCode?: string @@ -60,7 +61,6 @@ export interface LastMileOrderSummaryDto { transferWarehouseCode?: string | null fbaWarehouseCode?: string | null containerNo?: string | null - containerStatus?: string | null abnormal?: boolean | null detained?: boolean | null holdChargeable?: boolean | null @@ -77,6 +77,7 @@ export interface LastMileOrderSummaryDto { actualArrivalAt?: string | null estimatedWarehouseAt?: string | null inboundOrderNo?: string | null + dispatchStatus?: 'SCHEDULED' | 'UNSCHEDULED' | null /** Legacy read-only association retained for compatibility; absent from current service DTO. */ deliveryOrderNo?: string | null outboundOrderNo?: string | null diff --git a/src/modules/overseas-warehouse/api/warehouseTransfers.test.ts b/src/modules/overseas-warehouse/api/warehouseTransfers.test.ts new file mode 100644 index 00000000..ab84f311 --- /dev/null +++ b/src/modules/overseas-warehouse/api/warehouseTransfers.test.ts @@ -0,0 +1,28 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { overseasHttp } from '@/shared/api/overseasHttp' +import { getWarehouseTransfer, listWarehouseTransfers, reviewWarehouseTransfer } from './warehouseTransfers' + +vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), post: vi.fn() } })) + +describe('warehouseTransfers API', () => { + beforeEach(() => vi.clearAllMocks()) + + it('always scopes platform list and detail requests to the selected customer', async () => { + vi.mocked(overseasHttp.get).mockResolvedValueOnce({ code: '200', data: { records: [], page: 1, size: 10, total: 0, pages: 0 } }) + vi.mocked(overseasHttp.get).mockResolvedValueOnce({ code: '200', data: { transferNo: 'WT-1', items: [], cartons: [], relabelBindings: [] } }) + await listWarehouseTransfers('20001', { page: 1, size: 10, status: 'PENDING_REVIEW' }) + await getWarehouseTransfer('20001', 'WT-1') + expect(vi.mocked(overseasHttp.get).mock.calls[0]).toEqual(['/platform/v1/last-leg/warehouse-transfers', { params: { page: 1, size: 10, status: 'PENDING_REVIEW', customerId: '20001' } }]) + expect(vi.mocked(overseasHttp.get).mock.calls[1]).toEqual(['/platform/v1/last-leg/warehouse-transfers/WT-1', { params: { customerId: '20001' } }]) + }) + + it('posts the real review decision with customer scope', async () => { + vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', data: { transferNo: 'WT-1', items: [], cartons: [], relabelBindings: [] } }) + await reviewWarehouseTransfer('20001', 'WT-1', 'APPROVE', '资料无误') + expect(overseasHttp.post).toHaveBeenCalledWith( + '/platform/v1/last-leg/warehouse-transfers/WT-1/review', + { decision: 'APPROVE', remark: '资料无误' }, + { params: { customerId: '20001' } }, + ) + }) +}) diff --git a/src/modules/overseas-warehouse/api/warehouseTransfers.ts b/src/modules/overseas-warehouse/api/warehouseTransfers.ts new file mode 100644 index 00000000..10407f11 --- /dev/null +++ b/src/modules/overseas-warehouse/api/warehouseTransfers.ts @@ -0,0 +1,117 @@ +import { overseasHttp } from '@/shared/api/overseasHttp' +import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' +import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders' + +export interface WarehouseTransferQuery { + page: number + size: number + transferNo?: string + operatingWarehouseId?: string + fbaWarehouseCode?: string + status?: string + needRelabel?: boolean +} + +export interface WarehouseTransferSummaryDto { + id: string + transferNo: string + inboundCode?: string | null + lastlegOrderId?: string | null + lastlegOrderNo?: string | null + operatingWarehouseId?: string | null + fbaWarehouseCode?: string | null + packageCount: number + deliveryMethod?: string | null + needRelabel: boolean + relabelNo?: string | null + status: string + estimatedArrivalAt?: string | null + createdAt?: string | null +} + +export interface WarehouseTransferItemDto { + id: string + skuCode: string + productNameZh: string + packageCount: number + shippingMark?: string | null +} + +export interface WarehouseTransferCartonDto { + cartonMark?: string | null + customerCartonMark?: string | null + cartonIndex: number + totalCartons: number + status: string +} + +export interface WarehouseTransferRelabelBindingDto { + operationType: string + customerCartonMark: string + originalSku: string + newSku?: string | null + quantity: number + fbaCartonMark?: string | null + destinationWarehouseCode?: string | null + remark?: string | null +} + +export interface WarehouseTransferPalletDto { + id: string + sequenceNo: number + palletNo?: string | null + cartonCount: number + grossWeightKg: number + volumeCbm: number + status: string +} + +export interface WarehouseTransferDetailDto extends WarehouseTransferSummaryDto { + inboundOrderId?: string | null + orderType?: string | null + orderStatus?: string | null + relabelApplicationId?: string | null + relabelStatus?: string | null + deliveryAddress?: string | null + estimatedShipAt?: string | null + fbaShipmentNo?: string | null + trackingNo?: string | null + fbaBoxNos?: string | null + remark?: string | null + items: WarehouseTransferItemDto[] + cartons: WarehouseTransferCartonDto[] + pallets: WarehouseTransferPalletDto[] + relabelBindings: WarehouseTransferRelabelBindingDto[] + customerConfirmedAt?: string | null + actualShipAt?: string | null +} + +export async function listWarehouseTransfers(customerId: string, query: WarehouseTransferQuery) { + const response = await overseasHttp.get>, ApiEnvelope>>( + '/platform/v1/last-leg/warehouse-transfers', + { params: { ...query, customerId } }, + ) + return normalizeOverseasPage(unwrapApiResponse(response)) +} + +export async function getWarehouseTransfer(customerId: string, transferNo: string) { + const response = await overseasHttp.get, ApiEnvelope>( + `/platform/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}`, + { params: { customerId } }, + ) + return unwrapApiResponse(response) +} + +export async function reviewWarehouseTransfer( + customerId: string, + transferNo: string, + decision: 'APPROVE' | 'REJECT', + remark?: string, +) { + const response = await overseasHttp.post, ApiEnvelope>( + `/platform/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}/review`, + { decision, remark }, + { params: { customerId } }, + ) + return unwrapApiResponse(response) +} diff --git a/src/modules/overseas-warehouse/components/SmartDispatchDialog.vue b/src/modules/overseas-warehouse/components/SmartDispatchDialog.vue index c24c333a..b79df95d 100644 --- a/src/modules/overseas-warehouse/components/SmartDispatchDialog.vue +++ b/src/modules/overseas-warehouse/components/SmartDispatchDialog.vue @@ -8,16 +8,33 @@ import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' import AppDialog from '@/shared/components/AppDialog.vue' import LoadErrorState from '@/shared/components/LoadErrorState.vue' import OperationErrorToast from '@/shared/components/OperationErrorToast.vue' -import { createDispatchDraftContainer, deleteDispatchDraftContainer, listDispatchCandidates, listDispatchDrafts, lockDispatchDraftPallet, preArrangeDispatch, releaseDispatchDraft, unlockDispatchDraftPallet, type DispatchPalletCandidateDto } from '../api/dispatches' +import { createDispatchDraftContainer, deleteDispatchDraftContainer, listDispatchCandidates, listDispatchDrafts, lockDispatchDraftPallet, preArrangeDispatch, unlockDispatchDraftPallet, type DispatchPalletCandidateDto } from '../api/dispatches' import { formatOverseasDateTime } from '../lastMilePresentation' import type { InboundOrderListItem } from '../types' import OrderReferenceText from './OrderReferenceText.vue' -type VehicleSpec = { value: string; label: string; capacity: number; maxWeightLb: number } -interface LoadingContainer { id: string; capacity: number; maxWeightLb: number; vehicleLabel: string; palletIds: string[] } +type VehicleSpec = { + value: string + label: string + capacity: number + maxWeightLb: number +} +interface LoadingContainer { + id: string + capacity: number + maxWeightLb: number + vehicleLabel: string + palletIds: string[] +} -const props = defineProps<{ modelValue: boolean; orders: InboundOrderListItem[] }>() -const emit = defineEmits<{ 'update:modelValue': [value: boolean]; created: [] }>() +const props = defineProps<{ + modelValue: boolean + orders: InboundOrderListItem[] +}>() +const emit = defineEmits<{ + 'update:modelValue': [value: boolean] + created: [] +}>() const router = useRouter() const loading = ref(false) const saving = ref(false) @@ -29,10 +46,14 @@ const activeOrderKey = ref('') const containers = ref([]) const busyPalletIds = ref([]) const draftToken = ref('') -const finalized = ref(false) const vehicleDialogOpen = ref(false) const selectedVehicle = ref('') -const query = reactive({ orderNo: '', orderType: '', sourceStage: 'RECEIVED' as '' | 'PLAN' | 'RECEIVED', fbaWarehouseCode: '' }) +const query = reactive({ + orderNo: '', + orderType: '', + sourceStage: 'RECEIVED' as '' | 'PLAN' | 'RECEIVED', + fbaWarehouseCode: '', +}) const warehouseTab = ref<'ALL' | 'LAX' | 'NYC'>('ALL') const orderTypeOptions = [ @@ -44,22 +65,26 @@ const orderTypeOptions = [ ] const planIds = computed(() => [...new Set(props.orders.map((item) => item.planId).filter(Boolean))]) -const visibleCandidates = computed(() => candidates.value.filter((item) => { - const typeMatches = !query.orderType || String(item.orderType || '') === query.orderType - if (!typeMatches) return false - if (warehouseTab.value === 'ALL') return true - const warehouse = `${item.overseasWarehouseId || ''} ${item.overseasWarehouseName || ''}`.toLowerCase() - return warehouseTab.value === 'LAX' - ? /lax|洛杉矶|los angeles/.test(warehouse) - : /nyc|jfk|纽约|new york/.test(warehouse) -})) +const visibleCandidates = computed(() => + candidates.value.filter((item) => { + const typeMatches = !query.orderType || String(item.orderType || '') === query.orderType + if (!typeMatches) return false + if (warehouseTab.value === 'ALL') return true + const warehouse = `${item.overseasWarehouseId || ''} ${item.overseasWarehouseName || ''}`.toLowerCase() + return warehouseTab.value === 'LAX' ? /lax|洛杉矶|los angeles/.test(warehouse) : /nyc|jfk|纽约|new york/.test(warehouse) + }), +) const orderGroups = computed(() => { const groups = new Map() for (const item of visibleCandidates.value) { const key = String(item.orderId || item.orderNo || item.planId) groups.set(key, [...(groups.get(key) || []), item]) } - return [...groups.entries()].map(([key, pallets]) => ({ key, first: pallets[0]!, pallets })) + return [...groups.entries()].map(([key, pallets]) => ({ + key, + first: pallets[0]!, + pallets, + })) }) const activePallets = computed(() => orderGroups.value.find((item) => item.key === activeOrderKey.value)?.pallets || []) const assignedIds = computed(() => containers.value.flatMap((item) => item.palletIds)) @@ -67,95 +92,219 @@ const assigned = computed(() => assignedIds.value.map((id) => palletSnapshot(id) const assignedPackages = computed(() => assigned.value.reduce((sum, item) => sum + Number(item.packageCount || 0), 0)) const assignedVolume = computed(() => assigned.value.reduce((sum, item) => sum + Number(item.volumeCbm || 0), 0)) const vehicleSpecs: VehicleSpec[] = [ - { value: 'LARGE_30', label: '大车(30托)', capacity: 30, maxWeightLb: 40000 }, - { value: 'SMALL_15', label: '小车(15托)', capacity: 15, maxWeightLb: 10000 }, - { value: 'SMALL_14', label: '小车(14托)', capacity: 14, maxWeightLb: 10000 }, - { value: 'SMALL_12', label: '小车(12托)', capacity: 12, maxWeightLb: 10000 }, + { + value: 'LARGE_30', + label: '大车(30托)', + capacity: 30, + maxWeightLb: 40000, + }, + { + value: 'SMALL_15', + label: '小车(15托)', + capacity: 15, + maxWeightLb: 10000, + }, + { + value: 'SMALL_14', + label: '小车(14托)', + capacity: 14, + maxWeightLb: 10000, + }, + { + value: 'SMALL_12', + label: '小车(12托)', + capacity: 12, + maxWeightLb: 10000, + }, ] const selectedVehicleSpec = computed(() => vehicleSpecs.find((item) => item.value === selectedVehicle.value)) const weightLb = (item?: DispatchPalletCandidateDto) => Number(item?.volumeWeightKg || 0) * 2.2046226218 const containerWeightLb = (container: LoadingContainer) => container.palletIds.reduce((sum, id) => sum + weightLb(palletSnapshot(id)), 0) -function palletId(item: DispatchPalletCandidateDto) { return String(item.planPalletId) } -function text(value: unknown) { const normalized = String(value ?? '').trim(); return normalized || '—' } -function number(value: unknown, digits = 0) { const normalized = Number(value); return Number.isFinite(normalized) ? normalized.toFixed(digits) : '0' } -function dateTime(value: unknown) { return formatOverseasDateTime(value) } -function orderType(value: unknown) { return ({ FIRST_LEG: '头程订单', RELABEL_APPLICATION: '换标下单', RETURN_RELABEL: '退货换标', LOCAL_LOOSEBOARD: '本土散板', WAREHOUSE_TRANSFER: '仓储转运', ONE_CLICK_DROPSHIP: '一件代发', REJECTED: '拒收订单', EXPRESS: '快递订单' } as Record)[String(value || '')] || text(value) } -function cartonMarks(item: DispatchPalletCandidateDto) { return String(item.cartonMarks || '').split(',').map((value) => value.trim()).filter(Boolean) } -function containerFor(id: string) { return containers.value.find((item) => item.palletIds.includes(id)) } -function palletSnapshot(id: string) { return palletSnapshots.value[id] } -function makeDraftToken() { return globalThis.crypto?.randomUUID?.() || `dispatch-${Date.now()}-${Math.random().toString(16).slice(2)}` } +function palletId(item: DispatchPalletCandidateDto) { + return String(item.planPalletId || item.warehousePalletId || '') +} +function text(value: unknown) { + const normalized = String(value ?? '').trim() + return normalized || '—' +} +function number(value: unknown, digits = 0) { + const normalized = Number(value) + return Number.isFinite(normalized) ? normalized.toFixed(digits) : '0' +} +function dateTime(value: unknown) { + return formatOverseasDateTime(value) +} +function orderType(value: unknown) { + return ( + ( + { + FIRST_LEG: '头程订单', + RELABEL_APPLICATION: '换标下单', + RETURN_RELABEL: '退货换标', + LOCAL_LOOSEBOARD: '本土散板', + WAREHOUSE_TRANSFER: '仓储转运', + ONE_CLICK_DROPSHIP: '一件代发', + REJECTED: '拒收订单', + EXPRESS: '快递订单', + } as Record + )[String(value || '')] || text(value) + ) +} +function relabelCartonMarks(item: DispatchPalletCandidateDto) { + return String(item.relabelCartonMarks || '') + .split(',') + .map((value) => value.trim()) + .filter(Boolean) +} +function containerFor(id: string) { + return containers.value.find((item) => item.palletIds.includes(id)) +} +function palletSnapshot(id: string) { + return palletSnapshots.value[id] +} +function makeDraftToken() { + return globalThis.crypto?.randomUUID?.() || `dispatch-${Date.now()}-${Math.random().toString(16).slice(2)}` +} function ensureDraftToken() { if (!draftToken.value.trim()) draftToken.value = makeDraftToken() return draftToken.value } function rememberPallets(items: DispatchPalletCandidateDto[]) { - palletSnapshots.value = { ...palletSnapshots.value, ...Object.fromEntries(items.map((item) => [palletId(item), item])) } + palletSnapshots.value = { + ...palletSnapshots.value, + ...Object.fromEntries(items.map((item) => [palletId(item), item])), + } } async function hydrateDraftPallets() { - const restored = await listDispatchCandidates({ draftToken: ensureDraftToken() }) + const restored = await listDispatchCandidates({ + draftToken: ensureDraftToken(), + }) rememberPallets(restored) } async function load() { - loading.value = true; loadError.value = null + loading.value = true + loadError.value = null try { - candidates.value = await listDispatchCandidates({ planIds: planIds.value.length ? planIds.value.join(',') : undefined, orderNo: query.orderNo.trim() || undefined, fbaWarehouseCode: query.fbaWarehouseCode.trim() || undefined, sourceStage: query.sourceStage || undefined, draftToken: ensureDraftToken() }) + candidates.value = await listDispatchCandidates({ + planIds: planIds.value.length ? planIds.value.join(',') : undefined, + orderNo: query.orderNo.trim() || undefined, + fbaWarehouseCode: query.fbaWarehouseCode.trim() || undefined, + sourceStage: query.sourceStage || undefined, + draftToken: ensureDraftToken(), + }) rememberPallets(candidates.value) activeOrderKey.value = orderGroups.value[0]?.key || '' } catch (error) { - candidates.value = []; activeOrderKey.value = '' + candidates.value = [] + activeOrderKey.value = '' loadError.value = getApiErrorDetails(error, '可排车入仓单加载失败,请重试') - } finally { loading.value = false } + } finally { + loading.value = false + } +} +function reset() { + query.orderNo = '' + query.orderType = '' + query.sourceStage = 'RECEIVED' + query.fbaWarehouseCode = '' + warehouseTab.value = 'ALL' + void load() } -function reset() { query.orderNo = ''; query.orderType = ''; query.sourceStage = 'RECEIVED'; query.fbaWarehouseCode = ''; warehouseTab.value = 'ALL'; void load() } -function addContainer() { selectedVehicle.value = ''; vehicleDialogOpen.value = true } +function addContainer() { + selectedVehicle.value = '' + vehicleDialogOpen.value = true +} async function confirmAddContainer() { const spec = selectedVehicleSpec.value - if (!spec) { ElMessage.warning('请选择车型'); return } + if (!spec) { + ElMessage.warning('请选择车型') + return + } try { const created = await createDispatchDraftContainer(ensureDraftToken(), spec.capacity) - containers.value.push({ id: created.id, capacity: created.capacity, maxWeightLb: spec.maxWeightLb, vehicleLabel: spec.label, palletIds: created.palletIds || [] }) + containers.value.push({ + id: created.id, + capacity: created.capacity, + maxWeightLb: spec.maxWeightLb, + vehicleLabel: spec.label, + palletIds: created.palletIds || [], + }) vehicleDialogOpen.value = false - } catch (error) { mutationError.value = getApiErrorDetails(error, '添加装车车型失败,请重试') } + } catch (error) { + mutationError.value = getApiErrorDetails(error, '添加装车车型失败,请重试') + } } async function movePallet(item: DispatchPalletCandidateDto, containerId: string) { const id = palletId(item) const target = containers.value.find((container) => container.id === containerId) if (!target || target.palletIds.includes(id)) return - if (!String(item.overseasWarehouseId || '').trim()) { ElMessage.warning('该托盘尚未关联海外仓,不能排车'); return } - if (containerFor(id)) { ElMessage.warning('该托盘已在其他装车托盘中'); return } - if (target.palletIds.length >= target.capacity) { ElMessage.warning('该装车托盘已达到设置的托数'); return } + if (!String(item.overseasWarehouseId || '').trim()) { + ElMessage.warning('该托盘尚未关联海外仓,不能排车') + return + } + if (containerFor(id)) { + ElMessage.warning('该托盘已在其他装车托盘中') + return + } + if (target.palletIds.length >= target.capacity) { + ElMessage.warning('该装车托盘已达到设置的托数') + return + } const nextWeight = containerWeightLb(target) + weightLb(item) if (nextWeight > target.maxWeightLb * 1.1) ElMessage.error(`${target.vehicleLabel}重量将达到 ${nextWeight.toFixed(0)}LB,已超过限重10%,请注意调整`) else if (nextWeight > target.maxWeightLb) ElMessage.warning(`${target.vehicleLabel}当前重量 ${nextWeight.toFixed(0)}LB,已超过额定限重 ${target.maxWeightLb}LB`) const anchor = assigned.value[0] - if (anchor?.customerId && anchor.customerId !== item.customerId) { ElMessage.warning('一次排车只能装同一客户的托盘'); return } - if (anchor?.overseasWarehouseId && anchor.overseasWarehouseId !== item.overseasWarehouseId) { ElMessage.warning('一次排车只能装同一海外仓的托盘'); return } - busyPalletIds.value.push(id); mutationError.value = null - try { await lockDispatchDraftPallet(id, ensureDraftToken(), target.id); target.palletIds.push(id) } - catch (error) { mutationError.value = getApiErrorDetails(error, '托盘锁定失败,请刷新后重试') } - finally { busyPalletIds.value = busyPalletIds.value.filter((value) => value !== id) } + if (anchor?.customerId && anchor.customerId !== item.customerId) { + ElMessage.warning('一次排车只能装同一客户的托盘') + return + } + if (anchor?.overseasWarehouseId && anchor.overseasWarehouseId !== item.overseasWarehouseId) { + ElMessage.warning('一次排车只能装同一海外仓的托盘') + return + } + busyPalletIds.value.push(id) + mutationError.value = null + try { + if (item.sourceStage !== 'RELABEL') await lockDispatchDraftPallet(id, ensureDraftToken(), target.id) + target.palletIds.push(id) + } catch (error) { + mutationError.value = getApiErrorDetails(error, '托盘锁定失败,请刷新后重试') + } finally { + busyPalletIds.value = busyPalletIds.value.filter((value) => value !== id) + } } async function removePallet(id: string) { const owner = containerFor(id) if (!owner) return - busyPalletIds.value.push(id); mutationError.value = null - try { await unlockDispatchDraftPallet(id, ensureDraftToken()); owner.palletIds = owner.palletIds.filter((value) => value !== id) } - catch (error) { mutationError.value = getApiErrorDetails(error, '托盘解锁失败,请重试') } - finally { busyPalletIds.value = busyPalletIds.value.filter((value) => value !== id) } + busyPalletIds.value.push(id) + mutationError.value = null + try { + if (palletSnapshot(id)?.sourceStage !== 'RELABEL') await unlockDispatchDraftPallet(id, ensureDraftToken()) + owner.palletIds = owner.palletIds.filter((value) => value !== id) + } catch (error) { + mutationError.value = getApiErrorDetails(error, '托盘解锁失败,请重试') + } finally { + busyPalletIds.value = busyPalletIds.value.filter((value) => value !== id) + } } async function deleteContainer(container: LoadingContainer) { for (const id of [...container.palletIds]) { await removePallet(id) } - if (!container.palletIds.length) { await deleteDispatchDraftContainer(ensureDraftToken(), container.id); containers.value = containers.value.filter((item) => item.id !== container.id) } + if (!container.palletIds.length) { + await deleteDispatchDraftContainer(ensureDraftToken(), container.id) + containers.value = containers.value.filter((item) => item.id !== container.id) + } +} +function dragStart(event: DragEvent, item: DispatchPalletCandidateDto) { + event.dataTransfer?.setData('text/plain', palletId(item)) } -function dragStart(event: DragEvent, item: DispatchPalletCandidateDto) { event.dataTransfer?.setData('text/plain', palletId(item)) } function dropInto(event: DragEvent, containerId: string) { const id = event.dataTransfer?.getData('text/plain') || '' const item = candidates.value.find((candidate) => palletId(candidate) === id) @@ -163,31 +312,77 @@ function dropInto(event: DragEvent, containerId: string) { } async function close(value = false) { - const token = ensureDraftToken() - if (!value && token && !finalized.value) { - try { await releaseDispatchDraft(token) } catch { /* server-side expiry remains the fallback */ } - } emit('update:modelValue', value) } async function submit() { - if (!assignedIds.value.length) { ElMessage.warning('请至少移入一个托盘'); return } - if (assigned.value.some((item) => !String(item.overseasWarehouseId || '').trim())) { ElMessage.warning('所选托盘中存在尚未关联海外仓的托盘'); return } - saving.value = true; mutationError.value = null + if (!assignedIds.value.length) { + ElMessage.warning('请至少移入一个托盘') + return + } + if (assigned.value.some((item) => !String(item.overseasWarehouseId || '').trim())) { + ElMessage.warning('所选托盘中存在尚未关联海外仓的托盘') + return + } + const relabelOrders = [...new Set(assigned.value.filter((item) => item.sourceStage === 'RELABEL') + .map((item) => String(item.orderNo || '').trim()).filter(Boolean))] + if (relabelOrders.length && (relabelOrders.length !== 1 || assigned.value.some((item) => item.sourceStage !== 'RELABEL'))) { + ElMessage.warning('一张换标单的托盘需要单独排车') + return + } + saving.value = true + mutationError.value = null try { - await preArrangeDispatch({ planPalletIds: assignedIds.value, draftToken: ensureDraftToken() }) - finalized.value = true; emit('update:modelValue', false); emit('created'); ElMessage.success('预排车创建成功') + await preArrangeDispatch({ + planPalletIds: assignedIds.value, + ...(relabelOrders.length ? { relabelNo: relabelOrders[0] } : { draftToken: ensureDraftToken() }), + }) + emit('update:modelValue', false) + emit('created') + ElMessage.success('排车明细已同步到预排列表') await router.push({ name: 'overseas-dispatches' }) - } catch (error) { mutationError.value = getApiErrorDetails(error, '预排车创建失败,请重试') } - finally { saving.value = false } + } catch (error) { + mutationError.value = getApiErrorDetails(error, '预排车创建失败,请重试') + } finally { + saving.value = false + } } -watch(() => props.modelValue, (open) => { - if (!open) return - draftToken.value = makeDraftToken(); finalized.value = false; containers.value = []; palletSnapshots.value = {} - query.orderNo = ''; query.orderType = ''; query.sourceStage = 'RECEIVED'; query.fbaWarehouseCode = ''; warehouseTab.value = 'ALL'; mutationError.value = null - void (async () => { const drafts = await listDispatchDrafts().catch(() => []); const draft = drafts[0]; if (draft?.draftToken) { draftToken.value = draft.draftToken; containers.value = (draft.containers || []).map((c) => { const spec = vehicleSpecs.find((item) => item.capacity === c.capacity) || vehicleSpecs[0]!; return { id: c.id, capacity: c.capacity, maxWeightLb: spec.maxWeightLb, vehicleLabel: spec.label, palletIds: c.palletIds || [] } }); await hydrateDraftPallets().catch(() => undefined) } await load() })() -}, { immediate: true }) +watch( + () => props.modelValue, + (open) => { + if (!open) return + draftToken.value = makeDraftToken() + containers.value = [] + palletSnapshots.value = {} + query.orderNo = '' + query.orderType = '' + query.sourceStage = 'RECEIVED' + query.fbaWarehouseCode = '' + warehouseTab.value = 'ALL' + mutationError.value = null + void (async () => { + const drafts = await listDispatchDrafts().catch(() => []) + const draft = drafts[0] + if (draft?.draftToken) { + draftToken.value = draft.draftToken + containers.value = (draft.containers || []).map((c) => { + const spec = vehicleSpecs.find((item) => item.capacity === c.capacity) || vehicleSpecs[0]! + return { + id: c.id, + capacity: c.capacity, + maxWeightLb: spec.maxWeightLb, + vehicleLabel: spec.label, + palletIds: c.palletIds || [], + } + }) + await hydrateDraftPallets().catch(() => undefined) + } + await load() + })() + }, + { immediate: true }, +) watch([warehouseTab, () => query.orderType], () => { activeOrderKey.value = orderGroups.value[0]?.key || '' @@ -206,38 +401,111 @@ watch([warehouseTab, () => query.orderType], () => { - +
-

出库单列表 ({{ orderGroups.length }}单)

+
+
+

+ 出库单列表 ({{ orderGroups.length }}单) +

+
+ +
+
+
- - - - - - - - - + + + + + + + + + + +
-

托盘明细 {{ activePallets.length ? `(${activePallets.length}托)` : '' }}

订单号:
+
+

+ 托盘明细 + {{ activePallets.length ? `(${activePallets.length}托)` : '' }} +

+ 订单号: +
-
{{ text(item.palletNo) }}{{ Number(item.packageCount || 0) }}箱 · {{ number(weightLb(item), 0) }}LB
{{ mark }}+{{ cartonMarks(item).length - 4 }}暂无箱号
+
+ {{ text(item.palletNo) }}{{ Number(item.packageCount || 0) }}箱 · {{ number(weightLb(item), 0) }}LB +
+
+ {{ mark }}换标 +
+
+
已锁定 移入 - +
@@ -245,12 +513,41 @@ watch([warehouseTab, () => query.orderType], () => {
-
托盘移入即锁定,重量按 1kg = 2.2046lb 换算。
取消预排车
+
+ 托盘移入即锁定,重量按 1kg = 2.2046lb 换算。 +
取消预排车
+
@@ -270,41 +570,274 @@ watch([warehouseTab, () => query.orderType], () => { diff --git a/src/modules/overseas-warehouse/components/WarehouseTransferReviewDialog.vue b/src/modules/overseas-warehouse/components/WarehouseTransferReviewDialog.vue new file mode 100644 index 00000000..65d4e6b0 --- /dev/null +++ b/src/modules/overseas-warehouse/components/WarehouseTransferReviewDialog.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/src/modules/overseas-warehouse/lastMilePresentation.ts b/src/modules/overseas-warehouse/lastMilePresentation.ts index 12b98694..6490b9d3 100644 --- a/src/modules/overseas-warehouse/lastMilePresentation.ts +++ b/src/modules/overseas-warehouse/lastMilePresentation.ts @@ -100,6 +100,8 @@ export function toLastMileOrderListItem(dto: LastMileOrderSummaryDto): LastMileO customerId: String(dto.customerId), sourceContainerBookingId: String(dto.sourceContainerBookingId ?? '').trim(), orderNo: lastMileOrderReference(dto.orderNo), + orderTypeCode: String(dto.orderType ?? '').trim(), + currentStatusCode: String(dto.currentStatus ?? '').trim(), warning: dto.abnormal ? '异常' : '—', orderType: text(dto.orderTypeName || lastMileOrderTypeLabel(dto.orderType)), rejectedAt: '—', @@ -112,7 +114,6 @@ export function toLastMileOrderListItem(dto: LastMileOrderSummaryDto): LastMileO transferWarehouse: text(dto.transferWarehouseName), fbaWarehouse: text(dto.fbaWarehouseCode), containerNo: text(dto.containerNo), - containerStatus: text(dto.containerStatus), abnormal: yesNo(dto.abnormal), held: yesNo(dto.detained), detained: Boolean(dto.detained), @@ -129,6 +130,7 @@ export function toLastMileOrderListItem(dto: LastMileOrderSummaryDto): LastMileO portAta: formatOverseasDateTime(dto.actualArrivalAt), warehouseEta: formatOverseasDateTime(dto.estimatedWarehouseAt), inboundOrderNo: text(dto.inboundOrderNo), + dispatchStatus: dto.dispatchStatus === 'SCHEDULED' ? '已排车' : '未排车', outboundOrderNo: text(dto.outboundOrderNo), dispatchOrderNo: text(dto.deliveryOrderNo), relabelOrderNo: text(dto.relabelOrderNo), diff --git a/src/modules/overseas-warehouse/layoutAcceptance.test.ts b/src/modules/overseas-warehouse/layoutAcceptance.test.ts index 7569d5ec..0e6628d0 100644 --- a/src/modules/overseas-warehouse/layoutAcceptance.test.ts +++ b/src/modules/overseas-warehouse/layoutAcceptance.test.ts @@ -8,7 +8,7 @@ const component = (name: string) => readFileSync(resolve(process.cwd(), 'src/mod describe('overseas warehouse 1920px layout acceptance', () => { it('keeps action columns first and wide enough for every visible action', () => { const widths: Record = { - 'LastMileOrdersView.vue': 178, + 'LastMileOrdersView.vue': 230, 'InboundOrdersView.vue': 300, 'DispatchesView.vue': 270, 'OutboundsView.vue': 180, @@ -20,7 +20,7 @@ describe('overseas warehouse 1920px layout acceptance', () => { for (const [name, width] of Object.entries(widths)) { const source = page(name) const columns = source.slice(source.indexOf('const columns'), source.indexOf('async function', source.indexOf('const columns'))) - expect(columns).toContain(`{ key: 'actions', label: '操作', width: ${width}, fixed: 'left'`) + expect(columns).toMatch(new RegExp(`key: 'actions',[\\s\\S]*?label: '操作',[\\s\\S]*?width: ${width},[\\s\\S]*?fixed: 'left'`)) expect(columns.indexOf("key: 'actions'")).toBeLessThan(columns.indexOf("label:", columns.indexOf("key: 'actions'") + 20)) } }) diff --git a/src/modules/overseas-warehouse/pages/DispatchesView.test.ts b/src/modules/overseas-warehouse/pages/DispatchesView.test.ts index 025d7d2e..e8d22408 100644 --- a/src/modules/overseas-warehouse/pages/DispatchesView.test.ts +++ b/src/modules/overseas-warehouse/pages/DispatchesView.test.ts @@ -4,12 +4,12 @@ import { resolve } from 'node:path' import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import QueryToolbar, { type QueryModel } from '@/shared/components/QueryToolbar.vue' -import { cancelDispatch, listDispatches, transferDispatchToDelivery, updateDispatch } from '../api/dispatches' +import { cancelDispatch, listDispatches, removeDispatchItem, transferDispatchToDelivery, updateDispatch } from '../api/dispatches' import DispatchesView from './DispatchesView.vue' const canPermission = vi.hoisted(() => vi.fn(() => true)) vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: canPermission }) })) -vi.mock('../api/dispatches', () => ({ listDispatches: vi.fn(), updateDispatch: vi.fn(), cancelDispatch: vi.fn(), transferDispatchToDelivery: vi.fn() })) +vi.mock('../api/dispatches', () => ({ listDispatches: vi.fn(), updateDispatch: vi.fn(), removeDispatchItem: vi.fn(), cancelDispatch: vi.fn(), transferDispatchToDelivery: vi.fn() })) class ResizeObserverStub implements ResizeObserver { observe() {} @@ -47,6 +47,7 @@ describe('DispatchesView', () => { page: 1, size: 20, total: 1, pages: 1, }) vi.mocked(updateDispatch).mockReset() + vi.mocked(removeDispatchItem).mockReset() vi.mocked(cancelDispatch).mockReset() vi.mocked(transferDispatchToDelivery).mockReset() canPermission.mockReset().mockReturnValue(true) @@ -94,10 +95,13 @@ describe('DispatchesView', () => { expect(source).toContain('permission.dispatchEdit') expect(source).toContain('permission.dispatchCancel') expect(source).toContain('permission.dispatchTransferDelivery') - expect(source).toContain("function isPendingDeparture(row: DispatchListItem) { return row.statusCode === 'PENDING_DEPARTURE' }") - expect(source).toContain('access.can(permission.dispatchEdit) && isPendingDeparture(row)') - expect(source).toContain("access.can(permission.dispatchTransferDelivery) && ['PENDING_DEPARTURE', 'PICKING', 'READY_TO_DEPART'].includes(row.statusCode)") - expect(source).toContain('普通编辑不会修改排车状态') + expect(source).toMatch(/function isPendingDeparture\(row: DispatchListItem\)\s*{\s*return row\.statusCode === 'PENDING_DEPARTURE'/) + expect(source).toMatch(/access\.can\(permission\.dispatchEdit\)\s*&&\s*isPendingDeparture\(row\)/) + expect(source).toMatch(/access\.can\(permission\.dispatchTransferDelivery\)\s*&&\s*\['PENDING_DEPARTURE', 'PICKING', 'READY_TO_DEPART'\]\.includes\(row\.statusCode\)/) + expect(source).toContain('未生成出库单前可移除托盘') + expect(source).toContain('removeDispatchItem') + expect(source).not.toContain('托盘箱唛确认') + expect(source).not.toContain('palletCartonMarks') expect(source).not.toContain('departDispatch') canPermission.mockReturnValue(false) diff --git a/src/modules/overseas-warehouse/pages/DispatchesView.vue b/src/modules/overseas-warehouse/pages/DispatchesView.vue index 84b49946..1befef80 100644 --- a/src/modules/overseas-warehouse/pages/DispatchesView.vue +++ b/src/modules/overseas-warehouse/pages/DispatchesView.vue @@ -1,6 +1,6 @@ - + diff --git a/src/modules/overseas-warehouse/pages/LastMileOrderDetailView.vue b/src/modules/overseas-warehouse/pages/LastMileOrderDetailView.vue index 6886a3a6..c0e29b98 100644 --- a/src/modules/overseas-warehouse/pages/LastMileOrderDetailView.vue +++ b/src/modules/overseas-warehouse/pages/LastMileOrderDetailView.vue @@ -70,7 +70,6 @@ const containerFields = computed(() => { { label: '预计到港时间', value: formatOverseasDateTime(order?.estimatedArrivalAt) }, { label: '实际到港时间', value: formatOverseasDateTime(order?.actualArrivalAt) }, { label: '预计到仓时间', value: formatOverseasDateTime(order?.estimatedWarehouseAt) }, - { label: '清关状态', value: text(order?.containerStatus) }, ] }) const planFields = computed(() => { diff --git a/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts b/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts index 0ebb0f12..37fffd36 100644 --- a/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts +++ b/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts @@ -621,7 +621,7 @@ describe('LastMileOrdersView pending order and container conversion', () => { expect(source).toContain('.container-detail-summary > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }') expect(source).toContain('.container-workbench-card__warehouse strong { min-width: 0; overflow: hidden; color: var(--cc-text); font-size: 14px; text-overflow: ellipsis; white-space: nowrap; }') expect(source).toContain('.container-workbench-card__actions { display: flex; justify-content: flex-end; gap: 8px; padding-top: 2px; white-space: nowrap; }') - expect(source).toContain("{ key: 'actions', label: '操作', width: 178, fixed: 'left'") + expect(source).toContain("{ key: 'actions', label: '操作', width: 230, fixed: 'left'") }) it('loads only active FBA master data and submits exact string fields for a cross-customer transfer', async () => { diff --git a/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue b/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue index a7e6581f..8e7d9a6e 100644 --- a/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue +++ b/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue @@ -1,6 +1,6 @@ @@ -769,11 +780,12 @@ onMounted(() => { void load() })
- +
+ diff --git a/src/modules/overseas-warehouse/pages/ReturnInventoryManagementView.vue b/src/modules/overseas-warehouse/pages/ReturnInventoryManagementView.vue index 61b19c94..e50b0e52 100644 --- a/src/modules/overseas-warehouse/pages/ReturnInventoryManagementView.vue +++ b/src/modules/overseas-warehouse/pages/ReturnInventoryManagementView.vue @@ -2,8 +2,7 @@ import { computed, onMounted, reactive, ref } from 'vue' import { Printer, Search } from '@lucide/vue' import { ElMessage } from 'element-plus' -import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam' -import { listPlatformWarehouses, type PlatformWarehouseSummaryResource } from '@/modules/warehouses' +import { listPlatformTenants, listSharedReceivingOverseasWarehouseOptions, type PlatformTenantResource } from '@/modules/iam' import AdminPage from '@/shared/components/AdminPage.vue' import AppDialog from '@/shared/components/AppDialog.vue' import { getReturnCarton, getReturnCartonLabel, listReturnCartons, listReturnInventoryMovements, listReturnStock, type ReturnCartonDetail, type ReturnCartonRow, type ReturnInventoryMovement, type ReturnStockRow } from '../api/returnInventory' @@ -13,7 +12,7 @@ const stockRows = ref([]), cartonRows = ref const inventoryTab = ref<'stock' | 'movements'>('stock') const movementRows = ref([]) const customers = ref([]) -const warehouses = ref([]) +const warehouses = ref>([]) const optionsLoading = ref(false) const query = reactive({ page: 1, size: 20, customerId: '', overseasWarehouseId: '', skuCode: '', cartonMark: '', returnNo: '', status: 'AVAILABLE' }) function params() { return { ...query, customerId: query.customerId || undefined, overseasWarehouseId: query.overseasWarehouseId || undefined, skuCode: query.skuCode || undefined, cartonMark: query.cartonMark || undefined, returnNo: query.returnNo || undefined, status: query.status || undefined } } @@ -21,19 +20,25 @@ async function load() { loading.value = true; try { if (props.mode === 'inventor function search() { query.page = 1; void load() } function changeInventoryTab() { query.page = 1; void load() } const customerOptions = computed(() => customers.value.filter((item) => item.status === 'ACTIVE').map((item) => ({ label: item.tenantName, value: String(item.id) }))) -const warehouseOptions = computed(() => warehouses.value.filter((item) => item.status === 'ACTIVE' && item.warehouseType === 'OVERSEAS').map((item) => ({ label: item.warehouseName, value: String(item.id) }))) +const warehouseOptions = computed(() => warehouses.value.map((item) => ({ label: item.warehouseName, value: String(item.id) }))) function customerName(customerId?: string) { return customers.value.find((item) => String(item.id) === String(customerId))?.tenantName || '客户名称待补充' } function warehouseName(warehouseId?: string) { return warehouses.value.find((item) => String(item.id) === String(warehouseId))?.warehouseName || '海外仓名称待补充' } function statusLabel(status?: string) { return status === 'AVAILABLE' ? '可用' : status === 'MERGED' ? '已合箱' : '—' } async function loadOptions() { optionsLoading.value = true try { - const [customerPage, warehousePage] = await Promise.all([ + const [customerResult, warehouseResult] = await Promise.allSettled([ listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' }), - listPlatformWarehouses({ page: 1, size: 200, warehouseType: 'OVERSEAS', status: 'ACTIVE' }), + listSharedReceivingOverseasWarehouseOptions(), ]) - customers.value = customerPage.records - warehouses.value = warehousePage.records + if (customerResult.status === 'fulfilled') customers.value = customerResult.value.records + else customers.value = [] + if (warehouseResult.status === 'fulfilled') warehouses.value = warehouseResult.value + else warehouses.value = [] + if (customerResult.status === 'rejected' || warehouseResult.status === 'rejected') { + const failed = customerResult.status === 'rejected' ? '客户' : '海外仓' + ElMessage.warning(`${failed}筛选数据加载失败,请检查服务或数据库迁移`) + } } catch (error) { ElMessage.warning(error instanceof Error ? `筛选名称加载失败:${error.message}` : '筛选名称加载失败') } finally { optionsLoading.value = false } diff --git a/src/modules/overseas-warehouse/pages/prototypeContract.test.ts b/src/modules/overseas-warehouse/pages/prototypeContract.test.ts index d3b1a48e..53fb654f 100644 --- a/src/modules/overseas-warehouse/pages/prototypeContract.test.ts +++ b/src/modules/overseas-warehouse/pages/prototypeContract.test.ts @@ -14,6 +14,14 @@ function columnKeys(fileName: string) { } describe('overseas warehouse first-batch prototype contract', () => { + it('shows warehouse transfer review only for pending transfers with its dedicated authority', () => { + const value = source('LastMileOrdersView.vue') + expect(value).toContain('access.can(permission.warehouseTransferApprove)') + expect(value).toContain("row.orderTypeCode === 'WAREHOUSE_TRANSFER'") + expect(value).toContain("row.currentStatusCode === 'PENDING_REVIEW'") + expect(value).toContain(' { const expected = new Map([ ['LastMileOrdersView.vue', 'overseas-last-mile-orders-query'], @@ -247,7 +255,8 @@ describe('overseas warehouse first-batch prototype contract', () => { expect(dialog).toContain('planPalletIds: assignedIds.value') expect(dialog).toContain('lockDispatchDraftPallet') expect(dialog).toContain('unlockDispatchDraftPallet') - expect(dialog).toContain('releaseDispatchDraft') + expect(dialog).not.toContain('releaseDispatchDraft') + expect(dialog).toContain('排车明细已同步到预排列表') expect(dialog).toContain("router.push({ name: 'overseas-dispatches' })") expect(dialog).not.toContain('customerId:') expect(dialog).toContain("planIds: planIds.value.length ? planIds.value.join(',') : undefined") @@ -269,6 +278,11 @@ describe('overseas warehouse first-batch prototype contract', () => { expect(dialog).toContain('退货换标') expect(dialog).toContain('本土散板') expect(dialog).toContain('仓储转运') + expect(dialog).toContain('relabelCartonMarks(item)') + expect(dialog).toContain('type="warning">换标') + expect(dialog).not.toContain('function cartonMarks(') + expect(dialog).not.toContain('`normal-${mark}`') + expect(dialog).not.toContain('暂无箱唛') expect(dialog).toContain('class="warehouse-tabs"') expect(dialog).not.toContain('排车路线(选填)') expect(dialog).not.toContain('v-model="route"') @@ -355,7 +369,7 @@ describe('overseas warehouse first-batch prototype contract', () => { 'actions', 'selection', 'orderNo', 'consolidated', 'palletMergeRelation', 'warning', 'orderType', 'rejectedAt', 'inboundCode', 'lxCode', 'owner', 'warehouse', 'transferWarehouse', 'fbaWarehouse', 'containerNo', 'abnormal', 'held', 'pallets', 'cartons', 'volume', 'quantity', 'volumeWeight', 'portEta', 'portAta', 'warehouseEta', 'inboundOrderNo', - 'outboundOrderNo', 'dispatchOrderNo', 'relabelOrderNo', 'relabelStatus', 'status', + 'dispatchStatus', 'outboundOrderNo', 'dispatchOrderNo', 'relabelOrderNo', 'relabelStatus', 'status', 'createdAt', 'internalRemark', 'customerRemark', 'warehouseRemark', ]) expect(columnKeys('InboundOrdersView.vue')).toEqual([ diff --git a/src/modules/overseas-warehouse/permissions.ts b/src/modules/overseas-warehouse/permissions.ts index 2cf15ce8..14de1e43 100644 --- a/src/modules/overseas-warehouse/permissions.ts +++ b/src/modules/overseas-warehouse/permissions.ts @@ -2,6 +2,7 @@ export const overseasWarehousePermissions = { lastMileOrderList: 'platform:overseas-warehouse:last-mile-order:list', lastMileOrderExport: 'platform:overseas-warehouse:last-mile-order:export', lastMileOrderTransfer: 'platform:overseas-warehouse:last-mile-order:edit', + warehouseTransferApprove: 'platform:overseas-warehouse:warehouse-transfer:approve', inboundOrderList: 'platform:overseas-warehouse:inbound-order:list', inboundOrderCreate: 'platform:overseas-warehouse:inbound-order:create', inboundOrderEdit: 'platform:overseas-warehouse:inbound-order:edit', diff --git a/src/modules/overseas-warehouse/routes.test.ts b/src/modules/overseas-warehouse/routes.test.ts index 1b4b7e70..3b7f7c54 100644 --- a/src/modules/overseas-warehouse/routes.test.ts +++ b/src/modules/overseas-warehouse/routes.test.ts @@ -51,6 +51,7 @@ describe('overseas warehouse routes', () => { permission.customerVisitEdit, permission.dispatchEdit, permission.dispatchCancel, permission.dispatchTransferDelivery, permission.outboundCancel, permission.outboundCreate, - ]).size).toBe(17) + permission.warehouseTransferApprove, + ]).size).toBe(18) }) }) diff --git a/src/modules/overseas-warehouse/types.ts b/src/modules/overseas-warehouse/types.ts index 3efb0cf0..16332832 100644 --- a/src/modules/overseas-warehouse/types.ts +++ b/src/modules/overseas-warehouse/types.ts @@ -8,6 +8,8 @@ export interface LastMileOrderListItem { customerId: string sourceContainerBookingId: string orderNo: string + orderTypeCode: string + currentStatusCode: string warning: string orderType: string rejectedAt: string @@ -20,7 +22,6 @@ export interface LastMileOrderListItem { transferWarehouse: string fbaWarehouse: string containerNo: string - containerStatus: string abnormal: string held: string detained: boolean @@ -37,6 +38,7 @@ export interface LastMileOrderListItem { portAta: string warehouseEta: string inboundOrderNo: string + dispatchStatus: string /** Read-only association fact; this module does not implement the outbound workflow. */ outboundOrderNo: string /** Read-only association fact; this module does not implement overseas delivery. */