From e1b2f5155a2104f086ec9bf0483dce8304d9be28 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Mon, 28 Sep 2026 17:26:26 +0800 Subject: [PATCH 1/7] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../overseas-warehouse/api/lastMileOrders.ts | 9 +++++ .../overseas-warehouse/api/relabelOrders.ts | 9 +++++ .../api/warehouseTransfers.ts | 16 +++++++++ .../pages/LastMileOrdersView.behavior.test.ts | 15 ++++++++ .../pages/LastMileOrdersView.vue | 35 +++++++++++++++++-- .../pages/RelabelOrdersView.vue | 15 ++++++-- .../pages/prototypeContract.test.ts | 2 +- src/modules/overseas-warehouse/permissions.ts | 1 + src/modules/overseas-warehouse/types.ts | 3 ++ 9 files changed, 98 insertions(+), 7 deletions(-) diff --git a/src/modules/overseas-warehouse/api/lastMileOrders.ts b/src/modules/overseas-warehouse/api/lastMileOrders.ts index d5136bee..269cc51d 100644 --- a/src/modules/overseas-warehouse/api/lastMileOrders.ts +++ b/src/modules/overseas-warehouse/api/lastMileOrders.ts @@ -383,6 +383,15 @@ export async function reviewDirectRelabelOrder(orderId: string, customerId: stri return unwrapApiResponse(response) } +export async function reviewCargoDiscardOrder(discardNo: string, customerId: string, decision: 'APPROVE' | 'REJECT', remark?: string) { + const response = await overseasHttp.post, ApiEnvelope>( + `/platform/v1/last-leg/cargo-discards/${encodeURIComponent(discardNo)}/review`, + { decision, remark: remark?.trim() || undefined }, + { params: { customerId } }, + ) + return unwrapApiResponse(response) +} + export async function downloadLastMileOrders(query: Partial) { const { page: _page, size: _size, ...filters } = query const params = Object.fromEntries( diff --git a/src/modules/overseas-warehouse/api/relabelOrders.ts b/src/modules/overseas-warehouse/api/relabelOrders.ts index a79aa178..bafbfd2b 100644 --- a/src/modules/overseas-warehouse/api/relabelOrders.ts +++ b/src/modules/overseas-warehouse/api/relabelOrders.ts @@ -9,6 +9,12 @@ export interface RelabelOrderQuery { relabelNo?: string skuCode?: string status?: string + reviewedFrom?: string + reviewedTo?: string + measuredFrom?: string + measuredTo?: string + outboundFrom?: string + outboundTo?: string } export interface CreateRelabelItemPayload { @@ -82,6 +88,9 @@ export interface RelabelSummaryDto { customerConfirmedAt?: string | null reviewChannel?: 'TAIL_ORDER' | 'RELABEL_ORDER' | null reviewRemark?: string | null + reviewedAt?: string | null + measuredAt?: string | null + outboundAt?: string | null createdAt?: string | null } diff --git a/src/modules/overseas-warehouse/api/warehouseTransfers.ts b/src/modules/overseas-warehouse/api/warehouseTransfers.ts index 10407f11..57b72704 100644 --- a/src/modules/overseas-warehouse/api/warehouseTransfers.ts +++ b/src/modules/overseas-warehouse/api/warehouseTransfers.ts @@ -10,6 +10,12 @@ export interface WarehouseTransferQuery { fbaWarehouseCode?: string status?: string needRelabel?: boolean + reviewedFrom?: string + reviewedTo?: string + measuredFrom?: string + measuredTo?: string + outboundFrom?: string + outboundTo?: string } export interface WarehouseTransferSummaryDto { @@ -26,6 +32,12 @@ export interface WarehouseTransferSummaryDto { relabelNo?: string | null status: string estimatedArrivalAt?: string | null + estimatedShipAt?: string | null + actualShipAt?: string | null + reviewedAt?: string | null + measuredAt?: string | null + outboundAt?: string | null + relabelCompletedAt?: string | null createdAt?: string | null } @@ -84,6 +96,10 @@ export interface WarehouseTransferDetailDto extends WarehouseTransferSummaryDto relabelBindings: WarehouseTransferRelabelBindingDto[] customerConfirmedAt?: string | null actualShipAt?: string | null + reviewedAt?: string | null + measuredAt?: string | null + outboundAt?: string | null + relabelCompletedAt?: string | null } export async function listWarehouseTransfers(customerId: string, query: WarehouseTransferQuery) { diff --git a/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts b/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts index ec295788..ed74e72a 100644 --- a/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts +++ b/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts @@ -257,6 +257,21 @@ describe('LastMileOrdersView pending order and container conversion', () => { expect((button(wrapper, '生成入库计划')!.element as HTMLButtonElement).disabled).toBe(true) }) + it.each(['WAREHOUSE_TRANSFER', 'RELABEL_APPLICATION', 'RETURN_RELABEL'])( + 'does not offer an inbound plan for %s orders', + async (orderType) => { + canAccess.mockReturnValue(true) + vi.mocked(listLastMileOrders).mockResolvedValue({ records: [{ + id: `order-${orderType}`, customerId, orderNo: `LM-${orderType}`, currentStatus: 'PENDING_REVIEW', orderType, + }], page: 1, size: 10, total: 1, pages: 1 }) + + const wrapper = await mountView() + const row = (wrapper.findComponent(DataTable).props('data') as object[])[0] + expect(wrapper.get('.el-table__body-wrapper .row-actions').text()).not.toContain('生成入库计划') + expect((wrapper.vm as unknown as { canGenerateInboundPlan: (source: object) => boolean }).canGenerateInboundPlan(row)).toBe(false) + }, + ) + it('keeps the original pending order table and does not render a view switch or container table', async () => { const wrapper = await openPending() expect(wrapper.text()).toContain('尾程单号') diff --git a/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue b/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue index 71c1b291..1e6bb371 100644 --- a/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue +++ b/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue @@ -15,7 +15,7 @@ import LoadErrorState from '@/shared/components/LoadErrorState.vue' import OperationErrorToast from '@/shared/components/OperationErrorToast.vue' import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue' import RowActions, { type RowActionItem } from '@/shared/components/RowActions.vue' -import { changeContainerReceivingWarehouse, convertLastMileContainerToInboundPlans, createManualInboundPlan, downloadLastMileOrders, getContainerInboundPlanPreview, getContainerPlanProgress, getLastMileOrderManualNodes, listContainerPlanProgress, listLastMileOrders, reviewDirectRelabelOrder, reviewReturnOrder, transferLastMileOrders, updateLastMileOrderHold, updateLastMileOrderManualNodes, type ContainerInboundPlanPreviewDto, type ContainerInboundPlanPreviewGroupDto, type ContainerInboundPlanPreviewOrderDto, type ContainerPlanProgressDetailDto, type ContainerPlanProgressSummaryDto, type ManualNodeCode } from '../api/lastMileOrders' +import { changeContainerReceivingWarehouse, convertLastMileContainerToInboundPlans, createManualInboundPlan, downloadLastMileOrders, getContainerInboundPlanPreview, getContainerPlanProgress, getLastMileOrderManualNodes, listContainerPlanProgress, listLastMileOrders, reviewCargoDiscardOrder, reviewDirectRelabelOrder, reviewReturnOrder, transferLastMileOrders, updateLastMileOrderHold, updateLastMileOrderManualNodes, type ContainerInboundPlanPreviewDto, type ContainerInboundPlanPreviewGroupDto, type ContainerInboundPlanPreviewOrderDto, type ContainerPlanProgressDetailDto, type ContainerPlanProgressSummaryDto, type ManualNodeCode } from '../api/lastMileOrders' import OrderReferenceText from '../components/OrderReferenceText.vue' import WarehouseTransferReviewDialog from '../components/WarehouseTransferReviewDialog.vue' import { confirmRelabelCompletion, getRelabelReviewTask, returnRelabelForRework } from '../api/relabelOrders' @@ -446,6 +446,12 @@ function isRejectedReturn(source: object) { } function hasSelectedRejectedReturn() { return selectedRows.value.some(isRejectedReturn) } function hasSelectedInboundPlan() { return selectedRows.value.some(hasInboundPlan) } +const inboundPlanExcludedOrderTypes = new Set(['WAREHOUSE_TRANSFER', 'RELABEL_APPLICATION', 'RETURN_RELABEL']) +function canGenerateInboundPlan(row: LastMileOrderListItem) { + // Older list responses expose the same value as orderType; keep the action + // gate correct while the API rollout is mixed across environments. + return !inboundPlanExcludedOrderTypes.has(String(row.orderTypeCode || row.orderType || '')) +} async function loadTransferWarehouseOptions() { transferWarehouseLoading.value = true transferWarehouseError.value = null @@ -500,6 +506,7 @@ function idempotencyKey(prefix: string) { function openInboundPlan(source?: object) { const target = source ? source as LastMileOrderListItem : (selectedRows.value.length === 1 ? selectedRows.value[0] : null) if (!target) { ElMessage.warning('生成入库计划时请选择 1 条尾程订单'); return } + if (!canGenerateInboundPlan(target)) { ElMessage.warning('当前业务单不需要生成入库计划'); return } if (isRejectedReturn(target)) { ElMessage.warning('已驳回的退货预报只能查看详情'); return } if (hasInboundPlan(target)) { ElMessage.warning('当前订单已提交入库计划,不能重复生成'); return } if (target.detained) { ElMessage.warning('订单处于暂扣状态,解除暂扣后才能生成入库计划'); return } @@ -825,6 +832,26 @@ async function reviewDirectRelabel(source: object) { } else if (error !== 'close') ElMessage.error(getApiErrorDetails(error, '贴标单审核失败').message) } } +async function reviewCargoDiscard(source: object) { + const row = source as LastMileOrderListItem + const discardNo = row.orderNo + if (!discardNo || discardNo === '—' || !row.customerId) return + try { + await ElMessageBox.confirm('审核通过后,货物废弃单将推送到 PDA,仓库按托盘填写实际废弃数量。', '货物废弃审核', { confirmButtonText: '审核通过', cancelButtonText: '驳回', distinguishCancelAndClose: true }) + await reviewCargoDiscardOrder(discardNo, row.customerId, 'APPROVE') + ElMessage.success('货物废弃单已审核通过,已推送 PDA 废弃出库作业') + await load() + } catch (error) { + if (error === 'cancel') { + try { + const { value } = await ElMessageBox.prompt('请填写驳回原因', '驳回货物废弃单', { inputType: 'textarea', inputValidator: (input) => Boolean(input?.trim()) || '请填写驳回原因', confirmButtonText: '确认驳回', cancelButtonText: '取消' }) + await reviewCargoDiscardOrder(discardNo, row.customerId, 'REJECT', value) + ElMessage.success('货物废弃单已驳回') + await load() + } catch (rejectError) { if (rejectError !== 'cancel' && rejectError !== 'close') ElMessage.error(getApiErrorDetails(rejectError, '货物废弃审核失败').message) } + } else if (error !== 'close') ElMessage.error(getApiErrorDetails(error, '货物废弃审核失败').message) + } +} function canConfirmRelabel(row: { relabelStatusCode?: string }) { return row.relabelStatusCode === 'CUSTOMER_CONFIRM_PENDING' } function canReworkRelabel(row: { relabelStatusCode?: string }) { return ['CUSTOMER_CONFIRM_PENDING', 'PROCESSING'].includes(row.relabelStatusCode ?? '') } function hasRelabelActions(row: LastMileOrderListItem) { @@ -851,8 +878,9 @@ function lastMileRowActions(row: LastMileOrderListItem): RowActionItem[] { { key: 'detail', label: '查看', priority: 500 }, { key: 'review-direct-relabel', label: '审核', priority: 470, tone: 'warning', visible: access.can(permission.lastMileOrderTransfer) && row.orderTypeCode === 'RELABEL_APPLICATION' && row.currentStatusCode === 'PENDING_REVIEW' }, { key: 'review-return', label: '审核', priority: 460, tone: 'warning', visible: access.can(permission.returnOrderReview) && row.orderTypeCode === 'RETURN_RELABEL' && row.currentStatusCode === 'PENDING_REVIEW' }, + { key: 'review-cargo-discard', label: '审核', priority: 455, tone: 'warning', visible: (access.can(permission.cargoDiscardApprove) || access.can(permission.warehouseTransferApprove)) && row.orderTypeCode === 'CARGO_DISCARD' && row.currentStatusCode === 'PENDING_REVIEW' }, { key: 'review', label: '审核', priority: 450, tone: 'warning', visible: access.can(permission.warehouseTransferApprove) && row.orderTypeCode === 'WAREHOUSE_TRANSFER' && row.currentStatusCode === 'PENDING_REVIEW' }, - { key: 'inbound-plan', label: '生成入库计划', priority: 400, visible: access.can(permission.inboundOrderCreate) && !inboundPlanCreated }, + { key: 'inbound-plan', label: '生成入库计划', priority: 400, visible: access.can(permission.inboundOrderCreate) && canGenerateInboundPlan(row) && !inboundPlanCreated }, { key: 'hold', label: row.detained ? '解除暂扣' : '暂扣订单', priority: 300, tone: 'warning', visible: access.can(permission.lastMileOrderTransfer) && (row.detained || row.currentStatusCode === 'INBOUND_COMPLETED') }, { key: 'manual-nodes', label: '人工修改节点', priority: 200, visible: access.can(permission.lastMileOrderTransfer) }, { key: 'confirm-relabel', label: '换标完成确认', priority: 150, visible: access.can(permission.relabelOrderStatus) && Boolean(row.relabelOrderNo) && canConfirmRelabel(row) }, @@ -863,6 +891,7 @@ function handleLastMileRowAction(command: string, row: LastMileOrderListItem) { if (command === 'detail') openDetail(row) else if (command === 'review-direct-relabel') void reviewDirectRelabel(row) else if (command === 'review-return') void reviewReturn(row) + else if (command === 'review-cargo-discard') void reviewCargoDiscard(row) else if (command === 'review') openTransferReview(row) else if (command === 'inbound-plan') openInboundPlan(row) else if (command === 'hold') openHold(row) @@ -962,7 +991,7 @@ onMounted(() => { void load() })
- + diff --git a/src/modules/overseas-warehouse/pages/RelabelOrdersView.vue b/src/modules/overseas-warehouse/pages/RelabelOrdersView.vue index a0c2748c..1b52d7bf 100644 --- a/src/modules/overseas-warehouse/pages/RelabelOrdersView.vue +++ b/src/modules/overseas-warehouse/pages/RelabelOrdersView.vue @@ -31,11 +31,14 @@ const warehousesLoading = ref(false) const page = ref(1) const pageSize = ref(10) const total = ref(0) -const filters = ref({ relabelNo: '', sku: '', status: '' }) +const filters = ref({ relabelNo: '', sku: '', status: '', reviewedDates: [], measuredDates: [], outboundDates: [] }) const fields = computed(() => [ { key: 'relabelNo', type: 'input', label: '换标单号', placeholder: '请输入换标单号', width: 220 }, { key: 'sku', type: 'input', label: '商品SKU', placeholder: '请输入商品SKU', width: 180 }, { key: 'status', type: 'select', label: '状态', placeholder: '全部状态', width: 160, options: [{ label: '待审核', value: 'PENDING_REVIEW' }, { label: '待处理', value: 'PENDING_PROCESSING' }, { label: '处理中', value: 'PROCESSING' }, { label: '换标完成', value: 'COMPLETED' }] }, + { key: 'reviewedDates', type: 'daterange', label: '审核时间', width: 240 }, + { key: 'measuredDates', type: 'daterange', label: '测量时间', width: 240 }, + { key: 'outboundDates', type: 'daterange', label: '出库时间', width: 240 }, ]) const columns: DataTableColumn[] = [ { key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' }, @@ -49,6 +52,9 @@ const columns: DataTableColumn[] = [ { key: 'labelRequirement', label: '贴标要求', minWidth: 150 }, { key: 'fileName', label: '上传文件', minWidth: 160 }, { key: 'status', label: '状态', width: 110 }, + { key: 'reviewedAt', label: '审核时间', minWidth: 170 }, + { key: 'measuredAt', label: '测量时间', minWidth: 170 }, + { key: 'outboundAt', label: '出库时间', minWidth: 170 }, { key: 'createdAt', label: '创建时间', minWidth: 170 }, ] const dialogOpen = ref(false) @@ -171,10 +177,13 @@ async function load(model: QueryModel = filters.value) { Object.assign(filters.value, model) loading.value = true; loadError.value = null try { - const result = await listRelabelOrders({ page: page.value, size: pageSize.value, relabelNo: String(filters.value.relabelNo || '').trim() || undefined, skuCode: String(filters.value.sku || '').trim() || undefined, status: String(filters.value.status || '').trim() || undefined }) + const reviewedDates = Array.isArray(filters.value.reviewedDates) ? filters.value.reviewedDates : [] + const measuredDates = Array.isArray(filters.value.measuredDates) ? filters.value.measuredDates : [] + const outboundDates = Array.isArray(filters.value.outboundDates) ? filters.value.outboundDates : [] + const result = await listRelabelOrders({ page: page.value, size: pageSize.value, relabelNo: String(filters.value.relabelNo || '').trim() || undefined, skuCode: String(filters.value.sku || '').trim() || undefined, status: String(filters.value.status || '').trim() || undefined, reviewedFrom: String(reviewedDates[0] || '') || undefined, reviewedTo: String(reviewedDates[1] || '') || undefined, measuredFrom: String(measuredDates[0] || '') || undefined, measuredTo: String(measuredDates[1] || '') || undefined, outboundFrom: String(outboundDates[0] || '') || undefined, outboundTo: String(outboundDates[1] || '') || undefined }) rows.value = result.records.map((item) => ({ customerId: '', orderNo: String(item.relabelNo), relatedOrderNo: String(item.lastlegOrderNo || item.customerOrderNo || '—'), sku: String(item.skuCodes || '—'), - productName: '—', cartons: Number(item.totalPackageCount || 0), budgetTotal: null, relabelCost: Number(item.chargeAmount || 0), walletStatus: walletStatus(item.walletStatus), walletErrorMessage: item.walletErrorMessage || '', labelRequirement: '—', fileName: '—', status: relabelStatus(item.status), rawStatus: String(item.status || ''), remark: '', + productName: '—', cartons: Number(item.totalPackageCount || 0), budgetTotal: null, relabelCost: Number(item.chargeAmount || 0), walletStatus: walletStatus(item.walletStatus), walletErrorMessage: item.walletErrorMessage || '', labelRequirement: '—', fileName: '—', status: relabelStatus(item.status), rawStatus: String(item.status || ''), remark: '', reviewedAt: item.reviewedAt, measuredAt: item.measuredAt, outboundAt: item.outboundAt, reviewChannel: item.reviewChannel || undefined, createdAt: formatOverseasDateTime(item.createdAt), })) diff --git a/src/modules/overseas-warehouse/pages/prototypeContract.test.ts b/src/modules/overseas-warehouse/pages/prototypeContract.test.ts index 66f1bfc5..06729649 100644 --- a/src/modules/overseas-warehouse/pages/prototypeContract.test.ts +++ b/src/modules/overseas-warehouse/pages/prototypeContract.test.ts @@ -409,7 +409,7 @@ describe('overseas warehouse first-batch prototype contract', () => { 'inTransit', 'updatedAt', ]) expect(columnKeys('RelabelOrdersView.vue')).toEqual([ - 'actions', 'orderNo', 'sku', 'productName', 'cartons', 'budgetTotal', 'relabelCost', 'walletStatus', 'labelRequirement', 'fileName', 'status', 'createdAt', + 'actions', 'orderNo', 'sku', 'productName', 'cartons', 'budgetTotal', 'relabelCost', 'walletStatus', 'labelRequirement', 'fileName', 'status', 'reviewedAt', 'measuredAt', 'outboundAt', 'createdAt', ]) expect(columnKeys('PalletsView.vue')).toEqual([ 'actions', 'code', 'type', 'warehouse', 'volume', 'purchasePrice', 'fbaWarehouse', 'status', 'remark', diff --git a/src/modules/overseas-warehouse/permissions.ts b/src/modules/overseas-warehouse/permissions.ts index e73a8a7c..1ca5f879 100644 --- a/src/modules/overseas-warehouse/permissions.ts +++ b/src/modules/overseas-warehouse/permissions.ts @@ -3,6 +3,7 @@ export const overseasWarehousePermissions = { lastMileOrderExport: 'platform:overseas-warehouse:last-mile-order:export', lastMileOrderTransfer: 'platform:overseas-warehouse:last-mile-order:edit', warehouseTransferApprove: 'platform:overseas-warehouse:warehouse-transfer:approve', + cargoDiscardApprove: 'platform:overseas-warehouse:discard: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/types.ts b/src/modules/overseas-warehouse/types.ts index 3dedbc66..e919bb4c 100644 --- a/src/modules/overseas-warehouse/types.ts +++ b/src/modules/overseas-warehouse/types.ts @@ -153,6 +153,9 @@ export interface RelabelOrderListItem { status: string rawStatus?: string reviewChannel?: 'TAIL_ORDER' | 'RELABEL_ORDER' + reviewedAt?: string | null + measuredAt?: string | null + outboundAt?: string | null remark: string createdAt: string } From 0798143bad3756a16c750a97f2cfe3e0d29bb869 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 28 Sep 2026 17:09:46 +0100 Subject: [PATCH 2/7] fix(admin): reuse master data in sailing forms --- .../pages/SailingCalendarView.vue | 128 ++++++++++++------ 1 file changed, 84 insertions(+), 44 deletions(-) diff --git a/src/modules/sailing-calendar/pages/SailingCalendarView.vue b/src/modules/sailing-calendar/pages/SailingCalendarView.vue index 759793e6..40d28e32 100644 --- a/src/modules/sailing-calendar/pages/SailingCalendarView.vue +++ b/src/modules/sailing-calendar/pages/SailingCalendarView.vue @@ -7,12 +7,11 @@ import AdminPage from '@/shared/components/AdminPage.vue' import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' import LoadErrorState from '@/shared/components/LoadErrorState.vue' import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' -import { getPlatformPriceBook, listPlatformPriceBooks } from '@/modules/fba-pricing' +import { listAllPlatformWarehouses, type PlatformWarehouseSummaryResource } from '@/modules/warehouses' import { useAccessPreviewStore } from '@/core/access/accessPreview' import { cancelSailing, copySailing, - createSailingChannel, createSailingTemplate, createSailing, generateSailingTemplate, @@ -23,12 +22,10 @@ import { listSailingCalendar, listSailingTemplates, previewSailingTemplateGeneration, - updateSailingChannel, updateSailingTemplate, updateSailing, } from '../api/platformSailingCalendar' -import type { PlatformPriceBookParsedChannel } from '@/modules/fba-pricing' -import type { SailingBookingAdvice, SailingCalendarEntry, SailingCalendarFilterOptions, SailingCalendarResponse, SailingChannelResource, SailingChannelSaveRequest, SailingChannelSourceType, SailingHistory, SailingSaveRequest, SailingStatus, SailingTemplateResource, SailingTemplateSaveRequest, SailingVolumeDetail } from '../types' +import type { SailingBookingAdvice, SailingCalendarEntry, SailingCalendarFilterOptions, SailingCalendarResponse, SailingChannelResource, SailingHistory, SailingSaveRequest, SailingStatus, SailingTemplateResource, SailingTemplateSaveRequest, SailingVolumeDetail } from '../types' interface CalendarCell { date: string; day: number; currentMonth: boolean; records: SailingCalendarEntry[] } @@ -62,11 +59,12 @@ const managementTab = ref<'channels' | 'templates'>('channels') const managementLoading = ref(false) const managementError = ref(null) const channels = ref([]) -const priceBookChannels = ref([]) -const channelDialogOpen = ref(false) -const channelSaving = ref(false) -const editingChannelId = ref('') -const channelForm = reactive({ channelCode: '', channelName: '', sourceType: 'SCHEDULE_ONLY', mappedChannelCode: '', defaultDeparturePortCode: '', defaultDestinationPortCode: '', defaultCutoffDays: 3, remark: '' }) +const warehouseOptions = ref([]) +const warehouseLoading = ref(false) +let warehouseRequest: Promise | null = null +const warehousesLoaded = ref(false) +const formOptionsLoading = ref(false) +const routeExpanded = ref([]) const templates = ref([]) const templateDialogOpen = ref(false) const templateSaving = ref(false) @@ -195,37 +193,53 @@ async function openManagement(tab: 'channels' | 'templates' = 'channels') { managementError.value = null managementLoading.value = true try { - const [channelRows, templateRows] = await Promise.all([listSailingChannels(), listSailingTemplates()]) + const [channelRows, templateRows] = await Promise.all([listSailingChannels(), listSailingTemplates(), ensureWarehouseOptions()]) channels.value = channelRows templates.value = templateRows } catch (cause) { managementError.value = getApiErrorDetails(cause, '渠道与周模板加载失败') } finally { managementLoading.value = false } } -async function toggleChannel(row: unknown) { - const channelRow = row as SailingChannelResource - try { - const updated = await updateSailingChannel(channelRow.id, { channelCode: channelRow.channelCode, channelName: channelRow.channelName, sourceType: channelRow.sourceType, mappedChannelCode: channelRow.mappedChannelCode, defaultDeparturePortCode: channelRow.defaultDeparturePortCode, defaultDestinationPortCode: channelRow.defaultDestinationPortCode, defaultCutoffDays: channelRow.defaultCutoffDays, remark: channelRow.remark, expectedVersion: channelRow.version, status: channelRow.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE' } as SailingChannelSaveRequest & { status: string }) - channelRow.status = updated.status - ElMessage.success(channelRow.status === 'ACTIVE' ? '渠道已启用' : '渠道已停用') - } catch (cause) { ElMessage.error(getApiErrorDetails(cause, '渠道状态更新失败').message) } +async function ensureWarehouseOptions() { + if (warehousesLoaded.value) return + if (warehouseRequest) return warehouseRequest + warehouseLoading.value = true + warehouseRequest = listAllPlatformWarehouses({ warehouseType: 'DOMESTIC', status: 'ACTIVE' }) + .then((rows) => { warehouseOptions.value = rows; warehousesLoaded.value = true }) + .finally(() => { warehouseLoading.value = false; warehouseRequest = null }) + return warehouseRequest } -function openChannelCreate() { editingChannelId.value = ''; Object.assign(channelForm, { channelCode: '', channelName: '', sourceType: 'SCHEDULE_ONLY' as SailingChannelSourceType, mappedChannelCode: '', defaultDeparturePortCode: '', defaultDestinationPortCode: '', defaultCutoffDays: 3, remark: '', expectedVersion: undefined, status: undefined }); channelDialogOpen.value = true } -function openChannelEdit(row: unknown) { const channelRow = row as SailingChannelResource; editingChannelId.value = channelRow.id; Object.assign(channelForm, { channelCode: channelRow.channelCode, channelName: channelRow.channelName, sourceType: channelRow.sourceType, mappedChannelCode: channelRow.mappedChannelCode || '', defaultDeparturePortCode: channelRow.defaultDeparturePortCode || '', defaultDestinationPortCode: channelRow.defaultDestinationPortCode || '', defaultCutoffDays: channelRow.defaultCutoffDays, remark: channelRow.remark || '', expectedVersion: channelRow.version, status: channelRow.status }); channelDialogOpen.value = true } -async function saveChannel() { - if (!channelForm.channelCode.trim() || !channelForm.channelName.trim() || channelForm.defaultCutoffDays < 0) { ElMessage.error('请填写渠道编码、名称和有效的提前截止天数'); return } - channelSaving.value = true - try { - const response = editingChannelId.value ? await updateSailingChannel(editingChannelId.value, channelForm) : await createSailingChannel(channelForm) - if (editingChannelId.value) channels.value = channels.value.map((item) => item.id === response.id ? response : item) - else channels.value = [...channels.value, response] - channelDialogOpen.value = false - ElMessage.success(editingChannelId.value ? '渠道已更新' : '渠道已创建') - editingChannelId.value = '' - } catch (cause) { ElMessage.error(getApiErrorDetails(cause, editingChannelId.value ? '渠道更新失败' : '渠道创建失败').message) } finally { channelSaving.value = false } +function warehouseLabel(id: string) { + const warehouse = warehouseOptions.value.find((item) => String(item.id) === String(id)) + return warehouse ? `${warehouse.warehouseName}(${warehouse.warehouseCode})` : '未关联收货仓' } -function openTemplateCreate() { +function applyWarehouse(id: string, target: { warehouseId: string; warehouseName?: string | null }) { + target.warehouseId = String(id || '') + const warehouse = warehouseOptions.value.find((item) => String(item.id) === String(id)) + target.warehouseName = warehouse?.warehouseName || target.warehouseName || '' +} +function syncTemplateDerivedFields() { + const channel = channels.value.find((item) => item.id === String(templateForm.channelId)) + if (channel) { + templateForm.departurePortCode = templateForm.departurePortCode || channel.defaultDeparturePortCode || '' + templateForm.destinationPortCode = templateForm.destinationPortCode || channel.defaultDestinationPortCode || '' + templateForm.vesselName = templateForm.vesselName || '待订舱确认' + } + const warehouse = warehouseOptions.value.find((item) => String(item.id) === String(templateForm.warehouseId)) + if (warehouse) templateForm.warehouseName = warehouse.warehouseName +} +function onTemplateChannelChanged(id: string) { + const channel = channels.value.find((item) => item.id === String(id)) + if (!channel) return + templateForm.cutoffDays = channel.defaultCutoffDays + templateForm.departurePortCode = channel.defaultDeparturePortCode || '' + templateForm.destinationPortCode = channel.defaultDestinationPortCode || '' + templateForm.vesselName = '待订舱确认' +} +function onTemplateWarehouseChanged(id: string) { applyWarehouse(id, templateForm) } +async function openTemplateCreate() { + await ensureWarehouseOptions() editingTemplateId.value = '' const activeChannel = channels.value.find((item) => item.status === 'ACTIVE') - Object.assign(templateForm, { templateName: '', channelId: activeChannel?.id || '', warehouseId: '', warehouseName: '', weekdays: [1], effectiveFrom: formatDate(today), effectiveTo: '', departureTime: '10:00:00', cutoffDays: activeChannel?.defaultCutoffDays ?? 3, cutoffLocalTime: '18:00:00', voyageRule: '', vesselName: '', departurePortCode: '', destinationPortCode: '', remark: '', expectedVersion: undefined }) + Object.assign(templateForm, { templateName: '', channelId: activeChannel?.id || '', warehouseId: '', warehouseName: '', weekdays: [1], effectiveFrom: formatDate(today), effectiveTo: '', departureTime: '10:00:00', cutoffDays: activeChannel?.defaultCutoffDays ?? 3, cutoffLocalTime: '18:00:00', voyageRule: '', vesselName: '待订舱确认', departurePortCode: activeChannel?.defaultDeparturePortCode || '', destinationPortCode: activeChannel?.defaultDestinationPortCode || '', remark: '', expectedVersion: undefined }) templatePreview.value = [] resetGenerationRange(templateForm.effectiveFrom) templateDialogOpen.value = true @@ -238,8 +252,8 @@ function openTemplateEdit(row: unknown, options: { preserveGenerationRange?: boo if (!options.preserveGenerationRange) resetGenerationRange(templateForm.effectiveFrom) templateDialogOpen.value = true } -function templatePayload() { return { ...templateForm, effectiveTo: templateForm.effectiveTo || null, weekdays: [...templateForm.weekdays].sort((a, b) => a - b) } } -function validateTemplate() { if (!templateForm.channelId || !templateForm.warehouseId || !templateForm.weekdays.length || !templateForm.effectiveFrom || !templateForm.departureTime || templateForm.cutoffDays < 0 || !templateForm.vesselName?.trim() || !templateForm.departurePortCode?.trim() || !templateForm.destinationPortCode?.trim()) { ElMessage.error('请补全渠道、收货仓、周几、默认船名、起运港、目的港、有效期和截止天数'); return false } if (templateForm.cutoffDays === 0 && templateForm.cutoffLocalTime >= templateForm.departureTime) { ElMessage.error('同日收货截止时刻必须早于开船时间'); return false } return true } +function templatePayload() { syncTemplateDerivedFields(); return { ...templateForm, effectiveTo: templateForm.effectiveTo || null, weekdays: [...templateForm.weekdays].sort((a, b) => a - b), voyageRule: templateForm.voyageRule || null, vesselName: templateForm.vesselName || '待订舱确认', remark: templateForm.remark || null } } +function validateTemplate() { syncTemplateDerivedFields(); if (!templateForm.channelId || !templateForm.warehouseId || !templateForm.weekdays.length || !templateForm.effectiveFrom || !templateForm.departureTime || templateForm.cutoffDays < 0 || !templateForm.departurePortCode?.trim() || !templateForm.destinationPortCode?.trim()) { ElMessage.error('请补全渠道、收货仓、周几、有效期、开船时间、起运港和目的港'); return false } if (templateForm.cutoffDays === 0 && templateForm.cutoffLocalTime >= templateForm.departureTime) { ElMessage.error('同日收货截止时刻必须早于开船时间'); return false } return true } async function saveTemplate() { if (!validateTemplate()) return templateSaving.value = true @@ -283,11 +297,38 @@ async function confirmTemplateGeneration() { } finally { templateGenerateLoading.value = false } } function formatWeekdays(value: unknown) { return Array.isArray(value) ? value.map((day: unknown) => ['一', '二', '三', '四', '五', '六', '日'][Number(day) - 1]).join('、') : '—' } -async function onChannelChanged(value: string) { const channel = priceBookChannels.value.find((item) => item.channelCode === value); form.channelName = channel?.channelName || ''; form.channelId = null } -async function openCreate() { dialogMode.value = 'create'; cutoffMode.value = 'rule'; cutoffDays.value = 3; cutoffLocalTime.value = '18:00:00'; Object.assign(form, blankForm()); try { if (!priceBookChannels.value.length) { const versions = await listPlatformPriceBooks({ customerId: '0', status: 'ACTIVE' }); const latest = versions.sort((a, b) => (b.publishedAt || b.createdAt || '').localeCompare(a.publishedAt || a.createdAt || '') || b.versionNo - a.versionNo)[0]; if (latest) priceBookChannels.value = (await getPlatformPriceBook(latest.id)).channels } } catch (cause) { ElMessage.error(getApiErrorDetails(cause, '价格表渠道加载失败').message) } dialogOpen.value = true } -function openEdit(record: SailingCalendarEntry) { dialogMode.value = 'edit'; cutoffMode.value = 'custom'; cutoffDays.value = 3; cutoffLocalTime.value = record.cutoffAt.slice(11, 19) || '18:00:00'; Object.assign(form, { templateId: record.templateId ?? null, channelId: record.channelId ?? null, warehouseId: record.warehouseId, warehouseName: record.warehouseName, channelCode: record.channelCode, channelName: record.channelName, vesselName: record.vesselName, voyageNo: record.voyageNo, departurePortCode: record.departurePortCode, departurePortName: record.departurePortName || '', destinationPortCode: record.destinationPortCode, destinationPortName: record.destinationPortName || '', etdAt: record.etdAt.slice(0, 19), etaAt: record.etaAt ?? null, cutoffAt: record.cutoffAt.slice(0, 19), sourceType: record.sourceType ?? 'MANUAL', plannedCapacityCbm: record.plannedCapacityCbm ?? null, remark: record.remark || '', expectedVersion: record.version }); dialogOpen.value = true } +function onChannelChanged(value: string) { + const channel = channels.value.find((item) => item.channelCode === value) + form.channelName = channel?.channelName || '' + form.channelId = channel?.id || null + form.departurePortCode = channel?.defaultDeparturePortCode || '' + form.destinationPortCode = channel?.defaultDestinationPortCode || '' + form.departurePortName = '' + form.destinationPortName = '' + form.vesselName = null + form.voyageNo = null + cutoffDays.value = channel?.defaultCutoffDays ?? 3 + syncCutoffFromRule() + routeExpanded.value = form.departurePortCode && form.destinationPortCode ? [] : ['route'] +} +async function openCreate() { + dialogMode.value = 'create' + cutoffMode.value = 'rule' + cutoffDays.value = 3 + cutoffLocalTime.value = '18:00:00' + Object.assign(form, blankForm(), { expectedVersion: undefined }) + routeExpanded.value = [] + dialogOpen.value = true + formOptionsLoading.value = true + try { + const [rows] = await Promise.all([listSailingChannels(), ensureWarehouseOptions()]) + channels.value = rows + } catch (cause) { ElMessage.error(getApiErrorDetails(cause, '排船资料加载失败,请关闭后重试').message) } + finally { formOptionsLoading.value = false } +} +async function openEdit(record: SailingCalendarEntry) { try { const [rows] = await Promise.all([listSailingChannels(), ensureWarehouseOptions()]); channels.value = rows } catch (cause) { ElMessage.error(getApiErrorDetails(cause, '排船资料加载失败').message); return } routeExpanded.value = []; dialogMode.value = 'edit'; cutoffMode.value = 'custom'; cutoffDays.value = 3; cutoffLocalTime.value = record.cutoffAt.slice(11, 19) || '18:00:00'; Object.assign(form, { templateId: record.templateId ?? null, channelId: record.channelId ?? null, warehouseId: record.warehouseId, warehouseName: record.warehouseName, channelCode: record.channelCode, channelName: record.channelName, vesselName: record.vesselName, voyageNo: record.voyageNo, departurePortCode: record.departurePortCode, departurePortName: record.departurePortName || '', destinationPortCode: record.destinationPortCode, destinationPortName: record.destinationPortName || '', etdAt: record.etdAt.slice(0, 19), etaAt: record.etaAt ?? null, cutoffAt: record.cutoffAt.slice(0, 19), sourceType: record.sourceType ?? 'MANUAL', plannedCapacityCbm: record.plannedCapacityCbm ?? null, remark: record.remark || '', expectedVersion: record.version }); dialogOpen.value = true } function onEtdChanged() { if (dialogMode.value === 'create' || cutoffMode.value === 'rule') syncCutoffFromRule() } -function validateForm() { const required: Array<[keyof SailingSaveRequest, string]> = [['warehouseId', '收货仓'], ['channelCode', '价格表渠道'], ['departurePortCode', '起运港'], ['destinationPortCode', '目的港'], ['etdAt', '开船时间'], ['cutoffAt', '截止时间']]; const missing = required.find(([key]) => !String(form[key] || '').trim()); if (missing) { ElMessage.error(`请填写${missing[1]}`); return false } if (new Date(form.cutoffAt).getTime() >= new Date(form.etdAt).getTime()) { ElMessage.error('收货截止时间必须早于预计开船时间'); return false } return true } +function validateForm() { const required: Array<[keyof SailingSaveRequest, string]> = [['warehouseId', '收货仓'], ['channelCode', '渠道'], ['departurePortCode', '起运港'], ['destinationPortCode', '目的港'], ['etdAt', '开船时间'], ['cutoffAt', '截止时间']]; const missing = required.find(([key]) => !String(form[key] || '').trim()); if (missing) { ElMessage.error(`请填写${missing[1]}`); return false } if (new Date(form.cutoffAt).getTime() >= new Date(form.etdAt).getTime()) { ElMessage.error('收货截止时间必须早于预计开船时间'); return false } return true } async function save() { if (!validateForm()) return saving.value = true @@ -380,15 +421,14 @@ onMounted(() => void load()) - 渠道名称和编码来自已解析的船期价格表。
按开船前 N 天自定义时间
提前天数2 天3 天4 天
截止时刻
建议截止{{ dateTime(form.cutoffAt) }}按开船本地日期倒推自然日
+ 选择已有渠道,自动带入排船默认资料。
{{ form.vesselName || '待订舱确认' }} / {{ form.voyageNo || '待订舱确认' }}
未确认船名、航次时可先排船,显示为待订舱确认。
按开船前 N 天自定义时间
提前天数2 天3 天4 天
截止时刻
建议截止{{ dateTime(form.cutoffAt) }}按开船本地日期倒推自然日
-
停用渠道仍保留历史船期,但不能新建模板或生成未来船期。新增渠道
按周几生成前先预览,生成操作使用服务端幂等键。新增周模板
- - 周{{ ['一', '二', '三', '四', '五', '六', '日'][day - 1] }}
2 天3 天4 天
快捷生成范围:未来 4 周未来 8 周未来 12 周

生成预览({{ templatePreview.length }} 条)

+
查看已有船期渠道及默认收货规则。
设置每周开船日和收货截止,预览后批量生成;重复日期自动跳过。新增周模板
+ 周{{ ['一', '二', '三', '四', '五', '六', '日'][day - 1] }}
2 天3 天4 天
快捷生成范围:未来 4 周未来 8 周未来 12 周

生成预览({{ templatePreview.length }} 条)

From d7489a20be7ca00f6c9de4417316cd1395e724c2 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 28 Sep 2026 17:11:47 +0100 Subject: [PATCH 3/7] fix(admin): type sailing warehouse change handler --- src/modules/sailing-calendar/pages/SailingCalendarView.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/sailing-calendar/pages/SailingCalendarView.vue b/src/modules/sailing-calendar/pages/SailingCalendarView.vue index 40d28e32..5385c88d 100644 --- a/src/modules/sailing-calendar/pages/SailingCalendarView.vue +++ b/src/modules/sailing-calendar/pages/SailingCalendarView.vue @@ -235,6 +235,7 @@ function onTemplateChannelChanged(id: string) { templateForm.vesselName = '待订舱确认' } function onTemplateWarehouseChanged(id: string) { applyWarehouse(id, templateForm) } +function onFormWarehouseChanged(id: string) { applyWarehouse(id, form) } async function openTemplateCreate() { await ensureWarehouseOptions() editingTemplateId.value = '' @@ -421,7 +422,7 @@ onMounted(() => void load()) - 选择已有渠道,自动带入排船默认资料。
{{ form.vesselName || '待订舱确认' }} / {{ form.voyageNo || '待订舱确认' }}
未确认船名、航次时可先排船,显示为待订舱确认。
按开船前 N 天自定义时间
提前天数2 天3 天4 天
截止时刻
建议截止{{ dateTime(form.cutoffAt) }}按开船本地日期倒推自然日
+ 选择已有渠道,自动带入排船默认资料。
{{ form.vesselName || '待订舱确认' }} / {{ form.voyageNo || '待订舱确认' }}
未确认船名、航次时可先排船,显示为待订舱确认。
按开船前 N 天自定义时间
提前天数2 天3 天4 天
截止时刻
建议截止{{ dateTime(form.cutoffAt) }}按开船本地日期倒推自然日
查看已有船期渠道及默认收货规则。
设置每周开船日和收货截止,预览后批量生成;重复日期自动跳过。新增周模板
From ab5a64c6f733726c62c10f812f3b696fe6b250e5 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 29 Sep 2026 03:49:09 +0100 Subject: [PATCH 4/7] fix(admin): compact sailing calendar toolbar --- .../sailing-calendar/pages/SailingCalendarView.vue | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/modules/sailing-calendar/pages/SailingCalendarView.vue b/src/modules/sailing-calendar/pages/SailingCalendarView.vue index 5385c88d..1eb84953 100644 --- a/src/modules/sailing-calendar/pages/SailingCalendarView.vue +++ b/src/modules/sailing-calendar/pages/SailingCalendarView.vue @@ -396,19 +396,19 @@ onMounted(() => void load())
+
+ 重置 + 渠道与周模板 + 按周批量生成 + 新增单个船次 +
-
-
- 重置 - 渠道与周模板 - 按周批量生成 - 新增单个船次
@@ -431,5 +431,5 @@ onMounted(() => void load()) From f89a04ad2f0277f8631b621b24c7a6adaf9ea8de Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 29 Sep 2026 03:52:50 +0100 Subject: [PATCH 5/7] fix(admin): improve calendar toolbar breakpoint --- src/modules/sailing-calendar/pages/SailingCalendarView.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/modules/sailing-calendar/pages/SailingCalendarView.vue b/src/modules/sailing-calendar/pages/SailingCalendarView.vue index 1eb84953..3e955347 100644 --- a/src/modules/sailing-calendar/pages/SailingCalendarView.vue +++ b/src/modules/sailing-calendar/pages/SailingCalendarView.vue @@ -431,5 +431,7 @@ onMounted(() => void load()) + +\n \ No newline at end of file From 8fe256372ce87c77d7dda7d02334ec324427b496 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 29 Sep 2026 03:58:24 +0100 Subject: [PATCH 6/7] fix(admin): separate sailing search and actions --- .../pages/SailingCalendarView.vue | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/modules/sailing-calendar/pages/SailingCalendarView.vue b/src/modules/sailing-calendar/pages/SailingCalendarView.vue index 3e955347..0babdce1 100644 --- a/src/modules/sailing-calendar/pages/SailingCalendarView.vue +++ b/src/modules/sailing-calendar/pages/SailingCalendarView.vue @@ -396,12 +396,6 @@ onMounted(() => void load())
-
- 重置 - 渠道与周模板 - 按周批量生成 - 新增单个船次 -
@@ -410,6 +404,12 @@ onMounted(() => void load())
+
+ 重置 + 渠道与周模板 + 按周批量生成 + 新增单个船次 +
@@ -431,7 +431,5 @@ onMounted(() => void load()) - -\n \ No newline at end of file From 584b74e5e16f777be4c03818c8f1592a99c08895 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 29 Sep 2026 04:18:46 +0100 Subject: [PATCH 7/7] test(admin): active price book sailing channels --- .../overseas-warehouse/api/lastMileOrders.ts | 9 ----- .../overseas-warehouse/api/relabelOrders.ts | 9 ----- .../api/warehouseTransfers.ts | 16 -------- .../pages/LastMileOrdersView.behavior.test.ts | 15 ------- .../pages/LastMileOrdersView.vue | 35 ++--------------- .../pages/RelabelOrdersView.vue | 15 ++----- .../pages/prototypeContract.test.ts | 2 +- src/modules/overseas-warehouse/permissions.ts | 1 - src/modules/overseas-warehouse/types.ts | 3 -- .../api/platformSailingCalendar.ts | 4 +- .../pages/SailingCalendarView.vue | 39 +++++++++++++------ 11 files changed, 36 insertions(+), 112 deletions(-) diff --git a/src/modules/overseas-warehouse/api/lastMileOrders.ts b/src/modules/overseas-warehouse/api/lastMileOrders.ts index 269cc51d..d5136bee 100644 --- a/src/modules/overseas-warehouse/api/lastMileOrders.ts +++ b/src/modules/overseas-warehouse/api/lastMileOrders.ts @@ -383,15 +383,6 @@ export async function reviewDirectRelabelOrder(orderId: string, customerId: stri return unwrapApiResponse(response) } -export async function reviewCargoDiscardOrder(discardNo: string, customerId: string, decision: 'APPROVE' | 'REJECT', remark?: string) { - const response = await overseasHttp.post, ApiEnvelope>( - `/platform/v1/last-leg/cargo-discards/${encodeURIComponent(discardNo)}/review`, - { decision, remark: remark?.trim() || undefined }, - { params: { customerId } }, - ) - return unwrapApiResponse(response) -} - export async function downloadLastMileOrders(query: Partial) { const { page: _page, size: _size, ...filters } = query const params = Object.fromEntries( diff --git a/src/modules/overseas-warehouse/api/relabelOrders.ts b/src/modules/overseas-warehouse/api/relabelOrders.ts index bafbfd2b..a79aa178 100644 --- a/src/modules/overseas-warehouse/api/relabelOrders.ts +++ b/src/modules/overseas-warehouse/api/relabelOrders.ts @@ -9,12 +9,6 @@ export interface RelabelOrderQuery { relabelNo?: string skuCode?: string status?: string - reviewedFrom?: string - reviewedTo?: string - measuredFrom?: string - measuredTo?: string - outboundFrom?: string - outboundTo?: string } export interface CreateRelabelItemPayload { @@ -88,9 +82,6 @@ export interface RelabelSummaryDto { customerConfirmedAt?: string | null reviewChannel?: 'TAIL_ORDER' | 'RELABEL_ORDER' | null reviewRemark?: string | null - reviewedAt?: string | null - measuredAt?: string | null - outboundAt?: string | null createdAt?: string | null } diff --git a/src/modules/overseas-warehouse/api/warehouseTransfers.ts b/src/modules/overseas-warehouse/api/warehouseTransfers.ts index 57b72704..10407f11 100644 --- a/src/modules/overseas-warehouse/api/warehouseTransfers.ts +++ b/src/modules/overseas-warehouse/api/warehouseTransfers.ts @@ -10,12 +10,6 @@ export interface WarehouseTransferQuery { fbaWarehouseCode?: string status?: string needRelabel?: boolean - reviewedFrom?: string - reviewedTo?: string - measuredFrom?: string - measuredTo?: string - outboundFrom?: string - outboundTo?: string } export interface WarehouseTransferSummaryDto { @@ -32,12 +26,6 @@ export interface WarehouseTransferSummaryDto { relabelNo?: string | null status: string estimatedArrivalAt?: string | null - estimatedShipAt?: string | null - actualShipAt?: string | null - reviewedAt?: string | null - measuredAt?: string | null - outboundAt?: string | null - relabelCompletedAt?: string | null createdAt?: string | null } @@ -96,10 +84,6 @@ export interface WarehouseTransferDetailDto extends WarehouseTransferSummaryDto relabelBindings: WarehouseTransferRelabelBindingDto[] customerConfirmedAt?: string | null actualShipAt?: string | null - reviewedAt?: string | null - measuredAt?: string | null - outboundAt?: string | null - relabelCompletedAt?: string | null } export async function listWarehouseTransfers(customerId: string, query: WarehouseTransferQuery) { diff --git a/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts b/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts index ed74e72a..ec295788 100644 --- a/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts +++ b/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts @@ -257,21 +257,6 @@ describe('LastMileOrdersView pending order and container conversion', () => { expect((button(wrapper, '生成入库计划')!.element as HTMLButtonElement).disabled).toBe(true) }) - it.each(['WAREHOUSE_TRANSFER', 'RELABEL_APPLICATION', 'RETURN_RELABEL'])( - 'does not offer an inbound plan for %s orders', - async (orderType) => { - canAccess.mockReturnValue(true) - vi.mocked(listLastMileOrders).mockResolvedValue({ records: [{ - id: `order-${orderType}`, customerId, orderNo: `LM-${orderType}`, currentStatus: 'PENDING_REVIEW', orderType, - }], page: 1, size: 10, total: 1, pages: 1 }) - - const wrapper = await mountView() - const row = (wrapper.findComponent(DataTable).props('data') as object[])[0] - expect(wrapper.get('.el-table__body-wrapper .row-actions').text()).not.toContain('生成入库计划') - expect((wrapper.vm as unknown as { canGenerateInboundPlan: (source: object) => boolean }).canGenerateInboundPlan(row)).toBe(false) - }, - ) - it('keeps the original pending order table and does not render a view switch or container table', async () => { const wrapper = await openPending() expect(wrapper.text()).toContain('尾程单号') diff --git a/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue b/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue index 18be6021..a44ce948 100644 --- a/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue +++ b/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue @@ -15,7 +15,7 @@ import LoadErrorState from '@/shared/components/LoadErrorState.vue' import OperationErrorToast from '@/shared/components/OperationErrorToast.vue' import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue' import RowActions, { type RowActionItem } from '@/shared/components/RowActions.vue' -import { changeContainerReceivingWarehouse, convertLastMileContainerToInboundPlans, createManualInboundPlan, downloadLastMileOrders, getContainerInboundPlanPreview, getContainerPlanProgress, getLastMileOrderManualNodes, listContainerPlanProgress, listLastMileOrders, reviewCargoDiscardOrder, reviewDirectRelabelOrder, reviewReturnOrder, transferLastMileOrders, updateLastMileOrderHold, updateLastMileOrderManualNodes, type ContainerInboundPlanPreviewDto, type ContainerInboundPlanPreviewGroupDto, type ContainerInboundPlanPreviewOrderDto, type ContainerPlanProgressDetailDto, type ContainerPlanProgressSummaryDto, type ManualNodeCode } from '../api/lastMileOrders' +import { changeContainerReceivingWarehouse, convertLastMileContainerToInboundPlans, createManualInboundPlan, downloadLastMileOrders, getContainerInboundPlanPreview, getContainerPlanProgress, getLastMileOrderManualNodes, listContainerPlanProgress, listLastMileOrders, reviewDirectRelabelOrder, reviewReturnOrder, transferLastMileOrders, updateLastMileOrderHold, updateLastMileOrderManualNodes, type ContainerInboundPlanPreviewDto, type ContainerInboundPlanPreviewGroupDto, type ContainerInboundPlanPreviewOrderDto, type ContainerPlanProgressDetailDto, type ContainerPlanProgressSummaryDto, type ManualNodeCode } from '../api/lastMileOrders' import OrderReferenceText from '../components/OrderReferenceText.vue' import WarehouseTransferReviewDialog from '../components/WarehouseTransferReviewDialog.vue' import { confirmRelabelCompletion, getRelabelReviewTask, returnRelabelForRework } from '../api/relabelOrders' @@ -446,12 +446,6 @@ function isRejectedReturn(source: object) { } function hasSelectedRejectedReturn() { return selectedRows.value.some(isRejectedReturn) } function hasSelectedInboundPlan() { return selectedRows.value.some(hasInboundPlan) } -const inboundPlanExcludedOrderTypes = new Set(['WAREHOUSE_TRANSFER', 'RELABEL_APPLICATION', 'RETURN_RELABEL']) -function canGenerateInboundPlan(row: LastMileOrderListItem) { - // Older list responses expose the same value as orderType; keep the action - // gate correct while the API rollout is mixed across environments. - return !inboundPlanExcludedOrderTypes.has(String(row.orderTypeCode || row.orderType || '')) -} async function loadTransferWarehouseOptions() { transferWarehouseLoading.value = true transferWarehouseError.value = null @@ -506,7 +500,6 @@ function idempotencyKey(prefix: string) { function openInboundPlan(source?: object) { const target = source ? source as LastMileOrderListItem : (selectedRows.value.length === 1 ? selectedRows.value[0] : null) if (!target) { ElMessage.warning('生成入库计划时请选择 1 条尾程订单'); return } - if (!canGenerateInboundPlan(target)) { ElMessage.warning('当前业务单不需要生成入库计划'); return } if (isRejectedReturn(target)) { ElMessage.warning('已驳回的退货预报只能查看详情'); return } if (hasInboundPlan(target)) { ElMessage.warning('当前订单已提交入库计划,不能重复生成'); return } if (target.detained) { ElMessage.warning('订单处于暂扣状态,解除暂扣后才能生成入库计划'); return } @@ -832,26 +825,6 @@ async function reviewDirectRelabel(source: object) { } else if (error !== 'close') ElMessage.error(getApiErrorDetails(error, '贴标单审核失败').message) } } -async function reviewCargoDiscard(source: object) { - const row = source as LastMileOrderListItem - const discardNo = row.orderNo - if (!discardNo || discardNo === '—' || !row.customerId) return - try { - await ElMessageBox.confirm('审核通过后,货物废弃单将推送到 PDA,仓库按托盘填写实际废弃数量。', '货物废弃审核', { confirmButtonText: '审核通过', cancelButtonText: '驳回', distinguishCancelAndClose: true }) - await reviewCargoDiscardOrder(discardNo, row.customerId, 'APPROVE') - ElMessage.success('货物废弃单已审核通过,已推送 PDA 废弃出库作业') - await load() - } catch (error) { - if (error === 'cancel') { - try { - const { value } = await ElMessageBox.prompt('请填写驳回原因', '驳回货物废弃单', { inputType: 'textarea', inputValidator: (input) => Boolean(input?.trim()) || '请填写驳回原因', confirmButtonText: '确认驳回', cancelButtonText: '取消' }) - await reviewCargoDiscardOrder(discardNo, row.customerId, 'REJECT', value) - ElMessage.success('货物废弃单已驳回') - await load() - } catch (rejectError) { if (rejectError !== 'cancel' && rejectError !== 'close') ElMessage.error(getApiErrorDetails(rejectError, '货物废弃审核失败').message) } - } else if (error !== 'close') ElMessage.error(getApiErrorDetails(error, '货物废弃审核失败').message) - } -} function canConfirmRelabel(row: { relabelStatusCode?: string }) { return row.relabelStatusCode === 'CUSTOMER_CONFIRM_PENDING' } function canReworkRelabel(row: { relabelStatusCode?: string }) { return ['CUSTOMER_CONFIRM_PENDING', 'PROCESSING'].includes(row.relabelStatusCode ?? '') } function hasRelabelActions(row: LastMileOrderListItem) { @@ -878,9 +851,8 @@ function lastMileRowActions(row: LastMileOrderListItem): RowActionItem[] { { key: 'detail', label: '查看', priority: 500 }, { key: 'review-direct-relabel', label: '审核', priority: 470, tone: 'warning', visible: access.can(permission.lastMileOrderTransfer) && row.orderTypeCode === 'RELABEL_APPLICATION' && row.currentStatusCode === 'PENDING_REVIEW' }, { key: 'review-return', label: '审核', priority: 460, tone: 'warning', visible: access.can(permission.returnOrderReview) && row.orderTypeCode === 'RETURN_RELABEL' && row.currentStatusCode === 'PENDING_REVIEW' }, - { key: 'review-cargo-discard', label: '审核', priority: 455, tone: 'warning', visible: (access.can(permission.cargoDiscardApprove) || access.can(permission.warehouseTransferApprove)) && row.orderTypeCode === 'CARGO_DISCARD' && row.currentStatusCode === 'PENDING_REVIEW' }, { key: 'review', label: '审核', priority: 450, tone: 'warning', visible: access.can(permission.warehouseTransferApprove) && row.orderTypeCode === 'WAREHOUSE_TRANSFER' && row.currentStatusCode === 'PENDING_REVIEW' }, - { key: 'inbound-plan', label: '生成入库计划', priority: 400, visible: access.can(permission.inboundOrderCreate) && canGenerateInboundPlan(row) && !inboundPlanCreated }, + { key: 'inbound-plan', label: '生成入库计划', priority: 400, visible: access.can(permission.inboundOrderCreate) && !inboundPlanCreated }, { key: 'hold', label: row.detained ? '解除暂扣' : '暂扣订单', priority: 300, tone: 'warning', visible: access.can(permission.lastMileOrderTransfer) && (row.detained || row.currentStatusCode === 'INBOUND_COMPLETED') }, { key: 'manual-nodes', label: '人工修改节点', priority: 200, visible: access.can(permission.lastMileOrderTransfer) }, { key: 'confirm-relabel', label: '换标完成确认', priority: 150, visible: access.can(permission.relabelOrderStatus) && Boolean(row.relabelOrderNo) && canConfirmRelabel(row) }, @@ -891,7 +863,6 @@ function handleLastMileRowAction(command: string, row: LastMileOrderListItem) { if (command === 'detail') openDetail(row) else if (command === 'review-direct-relabel') void reviewDirectRelabel(row) else if (command === 'review-return') void reviewReturn(row) - else if (command === 'review-cargo-discard') void reviewCargoDiscard(row) else if (command === 'review') openTransferReview(row) else if (command === 'inbound-plan') openInboundPlan(row) else if (command === 'hold') openHold(row) @@ -991,7 +962,7 @@ onMounted(() => { void load() })
- + diff --git a/src/modules/overseas-warehouse/pages/RelabelOrdersView.vue b/src/modules/overseas-warehouse/pages/RelabelOrdersView.vue index 1b52d7bf..a0c2748c 100644 --- a/src/modules/overseas-warehouse/pages/RelabelOrdersView.vue +++ b/src/modules/overseas-warehouse/pages/RelabelOrdersView.vue @@ -31,14 +31,11 @@ const warehousesLoading = ref(false) const page = ref(1) const pageSize = ref(10) const total = ref(0) -const filters = ref({ relabelNo: '', sku: '', status: '', reviewedDates: [], measuredDates: [], outboundDates: [] }) +const filters = ref({ relabelNo: '', sku: '', status: '' }) const fields = computed(() => [ { key: 'relabelNo', type: 'input', label: '换标单号', placeholder: '请输入换标单号', width: 220 }, { key: 'sku', type: 'input', label: '商品SKU', placeholder: '请输入商品SKU', width: 180 }, { key: 'status', type: 'select', label: '状态', placeholder: '全部状态', width: 160, options: [{ label: '待审核', value: 'PENDING_REVIEW' }, { label: '待处理', value: 'PENDING_PROCESSING' }, { label: '处理中', value: 'PROCESSING' }, { label: '换标完成', value: 'COMPLETED' }] }, - { key: 'reviewedDates', type: 'daterange', label: '审核时间', width: 240 }, - { key: 'measuredDates', type: 'daterange', label: '测量时间', width: 240 }, - { key: 'outboundDates', type: 'daterange', label: '出库时间', width: 240 }, ]) const columns: DataTableColumn[] = [ { key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' }, @@ -52,9 +49,6 @@ const columns: DataTableColumn[] = [ { key: 'labelRequirement', label: '贴标要求', minWidth: 150 }, { key: 'fileName', label: '上传文件', minWidth: 160 }, { key: 'status', label: '状态', width: 110 }, - { key: 'reviewedAt', label: '审核时间', minWidth: 170 }, - { key: 'measuredAt', label: '测量时间', minWidth: 170 }, - { key: 'outboundAt', label: '出库时间', minWidth: 170 }, { key: 'createdAt', label: '创建时间', minWidth: 170 }, ] const dialogOpen = ref(false) @@ -177,13 +171,10 @@ async function load(model: QueryModel = filters.value) { Object.assign(filters.value, model) loading.value = true; loadError.value = null try { - const reviewedDates = Array.isArray(filters.value.reviewedDates) ? filters.value.reviewedDates : [] - const measuredDates = Array.isArray(filters.value.measuredDates) ? filters.value.measuredDates : [] - const outboundDates = Array.isArray(filters.value.outboundDates) ? filters.value.outboundDates : [] - const result = await listRelabelOrders({ page: page.value, size: pageSize.value, relabelNo: String(filters.value.relabelNo || '').trim() || undefined, skuCode: String(filters.value.sku || '').trim() || undefined, status: String(filters.value.status || '').trim() || undefined, reviewedFrom: String(reviewedDates[0] || '') || undefined, reviewedTo: String(reviewedDates[1] || '') || undefined, measuredFrom: String(measuredDates[0] || '') || undefined, measuredTo: String(measuredDates[1] || '') || undefined, outboundFrom: String(outboundDates[0] || '') || undefined, outboundTo: String(outboundDates[1] || '') || undefined }) + const result = await listRelabelOrders({ page: page.value, size: pageSize.value, relabelNo: String(filters.value.relabelNo || '').trim() || undefined, skuCode: String(filters.value.sku || '').trim() || undefined, status: String(filters.value.status || '').trim() || undefined }) rows.value = result.records.map((item) => ({ customerId: '', orderNo: String(item.relabelNo), relatedOrderNo: String(item.lastlegOrderNo || item.customerOrderNo || '—'), sku: String(item.skuCodes || '—'), - productName: '—', cartons: Number(item.totalPackageCount || 0), budgetTotal: null, relabelCost: Number(item.chargeAmount || 0), walletStatus: walletStatus(item.walletStatus), walletErrorMessage: item.walletErrorMessage || '', labelRequirement: '—', fileName: '—', status: relabelStatus(item.status), rawStatus: String(item.status || ''), remark: '', reviewedAt: item.reviewedAt, measuredAt: item.measuredAt, outboundAt: item.outboundAt, + productName: '—', cartons: Number(item.totalPackageCount || 0), budgetTotal: null, relabelCost: Number(item.chargeAmount || 0), walletStatus: walletStatus(item.walletStatus), walletErrorMessage: item.walletErrorMessage || '', labelRequirement: '—', fileName: '—', status: relabelStatus(item.status), rawStatus: String(item.status || ''), remark: '', reviewChannel: item.reviewChannel || undefined, createdAt: formatOverseasDateTime(item.createdAt), })) diff --git a/src/modules/overseas-warehouse/pages/prototypeContract.test.ts b/src/modules/overseas-warehouse/pages/prototypeContract.test.ts index 06729649..66f1bfc5 100644 --- a/src/modules/overseas-warehouse/pages/prototypeContract.test.ts +++ b/src/modules/overseas-warehouse/pages/prototypeContract.test.ts @@ -409,7 +409,7 @@ describe('overseas warehouse first-batch prototype contract', () => { 'inTransit', 'updatedAt', ]) expect(columnKeys('RelabelOrdersView.vue')).toEqual([ - 'actions', 'orderNo', 'sku', 'productName', 'cartons', 'budgetTotal', 'relabelCost', 'walletStatus', 'labelRequirement', 'fileName', 'status', 'reviewedAt', 'measuredAt', 'outboundAt', 'createdAt', + 'actions', 'orderNo', 'sku', 'productName', 'cartons', 'budgetTotal', 'relabelCost', 'walletStatus', 'labelRequirement', 'fileName', 'status', 'createdAt', ]) expect(columnKeys('PalletsView.vue')).toEqual([ 'actions', 'code', 'type', 'warehouse', 'volume', 'purchasePrice', 'fbaWarehouse', 'status', 'remark', diff --git a/src/modules/overseas-warehouse/permissions.ts b/src/modules/overseas-warehouse/permissions.ts index 1ca5f879..e73a8a7c 100644 --- a/src/modules/overseas-warehouse/permissions.ts +++ b/src/modules/overseas-warehouse/permissions.ts @@ -3,7 +3,6 @@ export const overseasWarehousePermissions = { lastMileOrderExport: 'platform:overseas-warehouse:last-mile-order:export', lastMileOrderTransfer: 'platform:overseas-warehouse:last-mile-order:edit', warehouseTransferApprove: 'platform:overseas-warehouse:warehouse-transfer:approve', - cargoDiscardApprove: 'platform:overseas-warehouse:discard: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/types.ts b/src/modules/overseas-warehouse/types.ts index e919bb4c..3dedbc66 100644 --- a/src/modules/overseas-warehouse/types.ts +++ b/src/modules/overseas-warehouse/types.ts @@ -153,9 +153,6 @@ export interface RelabelOrderListItem { status: string rawStatus?: string reviewChannel?: 'TAIL_ORDER' | 'RELABEL_ORDER' - reviewedAt?: string | null - measuredAt?: string | null - outboundAt?: string | null remark: string createdAt: string } diff --git a/src/modules/sailing-calendar/api/platformSailingCalendar.ts b/src/modules/sailing-calendar/api/platformSailingCalendar.ts index c120a55f..f0d3d1c5 100644 --- a/src/modules/sailing-calendar/api/platformSailingCalendar.ts +++ b/src/modules/sailing-calendar/api/platformSailingCalendar.ts @@ -145,8 +145,8 @@ const exceptionPath = '/platform/v1/sailing-assignments/exceptions' function channel(raw: SailingChannelResource): SailingChannelResource { return { ...raw, id: String(raw.id), defaultCutoffDays: number(raw.defaultCutoffDays), version: number(raw.version) } } function template(raw: SailingTemplateResource): SailingTemplateResource { return { ...raw, id: String(raw.id), channelId: String(raw.channelId), warehouseId: String(raw.warehouseId), weekdays: (raw.weekdays || []).map((value) => number(value)), cutoffDays: number(raw.cutoffDays), version: number(raw.version) } } -export async function listSailingChannels() { - const response = await http.get, ApiEnvelope>(channelPath) +export async function listSailingChannels(warehouseId?: string) { + const response = await http.get, ApiEnvelope>(channelPath, { params: warehouseId ? { warehouseId } : undefined }) const result = unwrapApiResponse(response) as SailingChannelResource[] | { records?: SailingChannelResource[] } | null return (Array.isArray(result) ? result : result?.records || []).map(channel) } diff --git a/src/modules/sailing-calendar/pages/SailingCalendarView.vue b/src/modules/sailing-calendar/pages/SailingCalendarView.vue index 0babdce1..a9bfb8db 100644 --- a/src/modules/sailing-calendar/pages/SailingCalendarView.vue +++ b/src/modules/sailing-calendar/pages/SailingCalendarView.vue @@ -234,23 +234,38 @@ function onTemplateChannelChanged(id: string) { templateForm.destinationPortCode = channel.defaultDestinationPortCode || '' templateForm.vesselName = '待订舱确认' } -function onTemplateWarehouseChanged(id: string) { applyWarehouse(id, templateForm) } -function onFormWarehouseChanged(id: string) { applyWarehouse(id, form) } +async function loadChannelsForWarehouse(warehouseId?: string) { + channels.value = warehouseId ? await listSailingChannels(warehouseId) : [] +} +async function onTemplateWarehouseChanged(id: string) { + applyWarehouse(id, templateForm) + templateForm.channelId = '' + templateForm.departurePortCode = '' + templateForm.destinationPortCode = '' + await loadChannelsForWarehouse(id) +} +async function onFormWarehouseChanged(id: string) { + applyWarehouse(id, form) + form.channelCode = '' + form.channelId = null + await loadChannelsForWarehouse(id) +} async function openTemplateCreate() { await ensureWarehouseOptions() editingTemplateId.value = '' - const activeChannel = channels.value.find((item) => item.status === 'ACTIVE') - Object.assign(templateForm, { templateName: '', channelId: activeChannel?.id || '', warehouseId: '', warehouseName: '', weekdays: [1], effectiveFrom: formatDate(today), effectiveTo: '', departureTime: '10:00:00', cutoffDays: activeChannel?.defaultCutoffDays ?? 3, cutoffLocalTime: '18:00:00', voyageRule: '', vesselName: '待订舱确认', departurePortCode: activeChannel?.defaultDeparturePortCode || '', destinationPortCode: activeChannel?.defaultDestinationPortCode || '', remark: '', expectedVersion: undefined }) + Object.assign(templateForm, { templateName: '', channelId: '', warehouseId: '', warehouseName: '', weekdays: [1], effectiveFrom: formatDate(today), effectiveTo: '', departureTime: '10:00:00', cutoffDays: 3, cutoffLocalTime: '18:00:00', voyageRule: '', vesselName: '待订舱确认', departurePortCode: '', destinationPortCode: '', remark: '', expectedVersion: undefined }) + channels.value = [] templatePreview.value = [] resetGenerationRange(templateForm.effectiveFrom) templateDialogOpen.value = true } -function openTemplateEdit(row: unknown, options: { preserveGenerationRange?: boolean } = {}) { +async function openTemplateEdit(row: unknown, options: { preserveGenerationRange?: boolean } = {}) { const templateRow = row as SailingTemplateResource editingTemplateId.value = templateRow.id Object.assign(templateForm, { templateName: templateRow.templateName || '', channelId: templateRow.channelId, warehouseId: templateRow.warehouseId, warehouseName: templateRow.warehouseName || '', weekdays: [...templateRow.weekdays], effectiveFrom: templateRow.effectiveFrom, effectiveTo: templateRow.effectiveTo || '', departureTime: templateRow.departureTime, cutoffDays: templateRow.cutoffDays, cutoffLocalTime: templateRow.cutoffLocalTime, voyageRule: templateRow.voyageRule || '', vesselName: templateRow.vesselName || '', departurePortCode: templateRow.departurePortCode || '', destinationPortCode: templateRow.destinationPortCode || '', remark: templateRow.remark || '', expectedVersion: templateRow.version }) templatePreview.value = [] if (!options.preserveGenerationRange) resetGenerationRange(templateForm.effectiveFrom) + try { await loadChannelsForWarehouse(templateForm.warehouseId) } catch (cause) { ElMessage.error(getApiErrorDetails(cause, '渠道加载失败').message); return } templateDialogOpen.value = true } function templatePayload() { syncTemplateDerivedFields(); return { ...templateForm, effectiveTo: templateForm.effectiveTo || null, weekdays: [...templateForm.weekdays].sort((a, b) => a - b), voyageRule: templateForm.voyageRule || null, vesselName: templateForm.vesselName || '待订舱确认', remark: templateForm.remark || null } } @@ -318,16 +333,16 @@ async function openCreate() { cutoffDays.value = 3 cutoffLocalTime.value = '18:00:00' Object.assign(form, blankForm(), { expectedVersion: undefined }) + channels.value = [] routeExpanded.value = [] dialogOpen.value = true formOptionsLoading.value = true try { - const [rows] = await Promise.all([listSailingChannels(), ensureWarehouseOptions()]) - channels.value = rows + await ensureWarehouseOptions() } catch (cause) { ElMessage.error(getApiErrorDetails(cause, '排船资料加载失败,请关闭后重试').message) } finally { formOptionsLoading.value = false } } -async function openEdit(record: SailingCalendarEntry) { try { const [rows] = await Promise.all([listSailingChannels(), ensureWarehouseOptions()]); channels.value = rows } catch (cause) { ElMessage.error(getApiErrorDetails(cause, '排船资料加载失败').message); return } routeExpanded.value = []; dialogMode.value = 'edit'; cutoffMode.value = 'custom'; cutoffDays.value = 3; cutoffLocalTime.value = record.cutoffAt.slice(11, 19) || '18:00:00'; Object.assign(form, { templateId: record.templateId ?? null, channelId: record.channelId ?? null, warehouseId: record.warehouseId, warehouseName: record.warehouseName, channelCode: record.channelCode, channelName: record.channelName, vesselName: record.vesselName, voyageNo: record.voyageNo, departurePortCode: record.departurePortCode, departurePortName: record.departurePortName || '', destinationPortCode: record.destinationPortCode, destinationPortName: record.destinationPortName || '', etdAt: record.etdAt.slice(0, 19), etaAt: record.etaAt ?? null, cutoffAt: record.cutoffAt.slice(0, 19), sourceType: record.sourceType ?? 'MANUAL', plannedCapacityCbm: record.plannedCapacityCbm ?? null, remark: record.remark || '', expectedVersion: record.version }); dialogOpen.value = true } +async function openEdit(record: SailingCalendarEntry) { try { await ensureWarehouseOptions(); await loadChannelsForWarehouse(record.warehouseId) } catch (cause) { ElMessage.error(getApiErrorDetails(cause, '排船资料加载失败').message); return } routeExpanded.value = []; dialogMode.value = 'edit'; cutoffMode.value = 'custom'; cutoffDays.value = 3; cutoffLocalTime.value = record.cutoffAt.slice(11, 19) || '18:00:00'; Object.assign(form, { templateId: record.templateId ?? null, channelId: record.channelId ?? null, warehouseId: record.warehouseId, warehouseName: record.warehouseName, channelCode: record.channelCode, channelName: record.channelName, vesselName: record.vesselName, voyageNo: record.voyageNo, departurePortCode: record.departurePortCode, departurePortName: record.departurePortName || '', destinationPortCode: record.destinationPortCode, destinationPortName: record.destinationPortName || '', etdAt: record.etdAt.slice(0, 19), etaAt: record.etaAt ?? null, cutoffAt: record.cutoffAt.slice(0, 19), sourceType: record.sourceType ?? 'MANUAL', plannedCapacityCbm: record.plannedCapacityCbm ?? null, remark: record.remark || '', expectedVersion: record.version }); dialogOpen.value = true } function onEtdChanged() { if (dialogMode.value === 'create' || cutoffMode.value === 'rule') syncCutoffFromRule() } function validateForm() { const required: Array<[keyof SailingSaveRequest, string]> = [['warehouseId', '收货仓'], ['channelCode', '渠道'], ['departurePortCode', '起运港'], ['destinationPortCode', '目的港'], ['etdAt', '开船时间'], ['cutoffAt', '截止时间']]; const missing = required.find(([key]) => !String(form[key] || '').trim()); if (missing) { ElMessage.error(`请填写${missing[1]}`); return false } if (new Date(form.cutoffAt).getTime() >= new Date(form.etdAt).getTime()) { ElMessage.error('收货截止时间必须早于预计开船时间'); return false } return true } async function save() { @@ -422,14 +437,14 @@ onMounted(() => void load()) - 选择已有渠道,自动带入排船默认资料。
{{ form.vesselName || '待订舱确认' }} / {{ form.voyageNo || '待订舱确认' }}
未确认船名、航次时可先排船,显示为待订舱确认。
按开船前 N 天自定义时间
提前天数2 天3 天4 天
截止时刻
建议截止{{ dateTime(form.cutoffAt) }}按开船本地日期倒推自然日
+ 选择已有渠道,自动带入排船默认资料。
{{ form.vesselName || '待订舱确认' }} / {{ form.voyageNo || '待订舱确认' }}
未确认船名、航次时可先排船,显示为待订舱确认。
按开船前 N 天自定义时间
提前天数2 天3 天4 天
截止时刻
建议截止{{ dateTime(form.cutoffAt) }}按开船本地日期倒推自然日
-
查看已有船期渠道及默认收货规则。
设置每周开船日和收货截止,预览后批量生成;重复日期自动跳过。新增周模板
- 周{{ ['一', '二', '三', '四', '五', '六', '日'][day - 1] }}
2 天3 天4 天
快捷生成范围:未来 4 周未来 8 周未来 12 周

生成预览({{ templatePreview.length }} 条)

+
渠道只读展示当前生效的通用船期价格表;周模板按收货仓筛选可用渠道。
设置每周开船日和收货截止,预览后批量生成;重复日期自动跳过。新增周模板
+ 渠道来自当前生效的通用船期价格表,并按收货仓的有效报价筛选。周{{ ['一', '二', '三', '四', '五', '六', '日'][day - 1] }}
2 天3 天4 天
快捷生成范围:未来 4 周未来 8 周未来 12 周

生成预览({{ templatePreview.length }} 条)