diff --git a/src/modules/appointment-calendar/WorkSharedTripForm.test.ts b/src/modules/appointment-calendar/WorkSharedTripForm.test.ts index 151d97f3..3a5df8fc 100644 --- a/src/modules/appointment-calendar/WorkSharedTripForm.test.ts +++ b/src/modules/appointment-calendar/WorkSharedTripForm.test.ts @@ -1,7 +1,7 @@ import { flushPromises, mount } from '@vue/test-utils' import { beforeEach, expect, it, vi } from 'vitest' import WorkSharedTripForm from './WorkSharedTripForm.vue' -import { getDispatch, useDispatchCommandRecovery, type DispatchDetail } from '@/modules/overseas-warehouse' +import { getDispatch, useDispatchCommandRecovery, type CommandResult, type DispatchDetail } from '@/modules/overseas-warehouse' import type { LeaveGuard } from './useWorkDraft' const access = vi.hoisted(() => ({ allowed: true })) const identity = vi.hoisted(() => ({ id: '' })) @@ -35,3 +35,46 @@ it('retries lookup as a read and keeps the original detail navigation', async () vi.mocked(getDispatch).mockRejectedValueOnce(new Error('lookup failed')); const wrapper = render(); await flushPromises(); await wrapper.get('[role="alert"] button').trigger('click'); await flushPromises(); expect(getDispatch).toHaveBeenCalledTimes(2) await button(wrapper, '查看原排车单').trigger('click'); expect(wrapper.emitted('external')).toEqual([['/overseas-warehouse/dispatches/DS-60']]); wrapper.unmount() }) +it.each([false, true])('refreshes the original add-cargo gate after a rejection and preserves its message when GET fails: %s', async readFails => { + const wrapper = render(); await flushPromises(); await button(wrapper, '选择同仓未安排货物').trigger('click') + if (readFails) vi.mocked(getDispatch).mockRejectedValueOnce(new Error('read unavailable')) + else vi.mocked(getDispatch).mockResolvedValueOnce(detail('DS-60', false)) + const dialog = wrapper.getComponent({ name: 'DispatchWorkflowDialog' }) + dialog.vm.$emit('rejected', { message: '来源已变化,请重新核对', code: 'A0409' }) + dialog.vm.$emit('update:modelValue', false); await flushPromises() + expect(getDispatch).toHaveBeenCalledTimes(2) + expect(wrapper.emitted('updated')).toHaveLength(1) + expect(wrapper.text()).toContain('来源已变化,请重新核对') + expect(button(wrapper, '选择同仓未安排货物').attributes('disabled')).toBeDefined() + expect(wrapper.text()).not.toContain('原排车请求尚未核对') + if (!readFails) expect(wrapper.text()).toContain('货物已开始真实拣货') + wrapper.unmount() +}) +it('offers read refresh for proven add-cargo success while retaining the exact original command', async () => { + const recovery = useDispatchCommandRecovery(() => identity.id) + recovery.begin('DS-60', 'ADD_CARGO', { recordVersion: 0, idempotencyKey: 'known-key', cargo: [], batchAllocations: [] }) + recovery.pending.value!.attempted = true + const completed: CommandResult = { + dispatchNo: 'DS-60', action: 'ADD_CARGO', idempotencyKey: 'known-key', recordVersion: 1, status: 'ACTIVE', stage: 'BEFORE_LOADING', + occurredAt: '2026-10-20T00:00:00Z', effect: 'APPLIED', counts: { itemCount: 1, palletCount: 1, packageCount: 60, pickedPackageCount: 0, shippedPackageCount: 0 }, + replayed: false, alreadyArranged: false, result: { dispatchNo: 'DS-60', changedItemIds: [] }, + } + recovery.pending.value!.result = completed + const wrapper = render(); await flushPromises() + expect(wrapper.text()).toContain('操作已完成,页面资料待刷新') + expect(wrapper.text()).not.toContain('原排车请求尚未核对') + await button(wrapper, '刷新加货资料').trigger('click') + expect(wrapper.getComponent({ name: 'DispatchWorkflowDialog' }).props('modelValue')).toBe(true) + expect(recovery.pending.value!.body.idempotencyKey).toBe('known-key') + wrapper.unmount() +}) +it('clears a prior add-cargo rejection after the corrected original-dispatch workflow succeeds', async () => { + const wrapper = render(); await flushPromises() + const dialog = wrapper.getComponent({ name: 'DispatchWorkflowDialog' }) + dialog.vm.$emit('rejected', { message: '来源版本需重新核对', code: 'A0409' }); await flushPromises() + expect(wrapper.text()).toContain('来源版本需重新核对') + dialog.vm.$emit('saved'); await flushPromises() + expect(wrapper.text()).not.toContain('来源版本需重新核对') + expect(wrapper.emitted('updated')).toHaveLength(2) + wrapper.unmount() +}) diff --git a/src/modules/appointment-calendar/WorkSharedTripForm.vue b/src/modules/appointment-calendar/WorkSharedTripForm.vue index 16b0d4aa..72b899ab 100644 --- a/src/modules/appointment-calendar/WorkSharedTripForm.vue +++ b/src/modules/appointment-calendar/WorkSharedTripForm.vue @@ -11,6 +11,7 @@ const props = defineProps<{ dispatchNo: string; canWrite: boolean }>() const emit = defineEmits<{ updated: []; busy: [value: boolean]; external: [path: string] }>() const access = useAccessPreviewStore(), auth = useAuthStore(), recovery = useDispatchCommandRecovery(() => auth.user?.id || '') const detail = ref(null), open = ref(false), loading = ref(false), error = ref(null) +const rejectionError = ref(null) const readable = computed(() => access.can('platform:overseas-warehouse:dispatch:list')) const allowed = computed(() => props.canWrite && readable.value && access.can('platform:overseas-warehouse:dispatch:edit') && detail.value?.status === 'ACTIVE' && detail.value.stage === 'BEFORE_LOADING' && detail.value.actions.ADD_CARGO?.allowed === true) const busy = computed(() => loading.value || open.value || Boolean(recovery.pending.value)) @@ -29,11 +30,12 @@ async function load() { } catch (cause) { if (current === generation) error.value = getApiErrorDetails(cause, cause instanceof DispatchInputError ? cause.message : '本车加货资格读取失败') } finally { if (current === generation) loading.value = false } } -function prepare() { if (allowed.value && !loading.value && !recovery.pending.value) open.value = true } +function prepare() { if (allowed.value && !loading.value && !recovery.pending.value) { rejectionError.value = null; open.value = true } } function recover() { if (recovery.pending.value?.dispatchNo === props.dispatchNo && recovery.pending.value.action === 'ADD_CARGO') open.value = true } -function saved() { open.value = false; emit('updated'); void load() } +function saved() { open.value = false; rejectionError.value = null; emit('updated'); void load() } +function rejected(details: ApiErrorDetails) { rejectionError.value = details; emit('updated'); void load() } watch(() => props.dispatchNo, load, { immediate: true }) onBeforeUnmount(() => { generation++; emit('busy', false) }) @@ -42,10 +44,11 @@ onBeforeUnmount(() => { generation++; emit('busy', false) })

为原排车添加同仓货物

本车 {{ dispatchNo }}。只选择尚未安排的真实货物加入本车,不合并另一张排车单或搬迁已扫码货物。

箱数无法对应完整实物时,先在原库存作业完成拆托;预约分批不代替实物拆托。

+

{{ detail.vehicleInfo.vehicle?.plate || '车辆待维护' }} · {{ detail.counts.packageCount }} 箱

{{ detail.actions.ADD_CARGO?.blockReason || '当前阶段或权限不允许加货,请从原排车单继续作业。' }}

-

原排车请求尚未核对,不能另发加货命令。核对原加货请求

+

{{ recovery.pending.value.result ? '原排车操作已完成,页面资料待刷新,不能另发加货命令。' : '原排车请求尚未核对,不能另发加货命令。' }}{{ recovery.pending.value.result ? '刷新加货资料' : '核对原加货请求' }}

选择同仓未安排货物查看原排车单 - + diff --git a/src/modules/fee-items/AGENTS.md b/src/modules/fee-items/AGENTS.md index 921f172d..bd147082 100644 --- a/src/modules/fee-items/AGENTS.md +++ b/src/modules/fee-items/AGENTS.md @@ -11,12 +11,12 @@ | 目标 | 页面/入口 | API 与后端 | |---|---|---| | 费用项目列表、启停与排序 | `pages/FeeItemsView.vue`;`master-data/fee-items` | `api/platformFeeItems.ts`;`firstleg/controller/PlatformFeeItemController.java` | -| 新增/编辑费用项目弹框 | `pages/FeeItemsView.vue` 内弹框 | `api/platformFeeItems.ts` 的 `FeeItemSaveRequest`(`feeCode` 可改) | +| 新增/编辑费用项目弹框 | `pages/FeeItemsView.vue` 内弹框 | `api/platformFeeItems.ts` 的 `SavePlatformFeeItemRequest`;当前表单不提交 `feeCode` | | 默认价格(CNY,可空)与类别/贸易范围 | 列表与新增/编辑弹框 | `api/platformFeeItems.ts`;带价与校验规则见详细索引 | ## 关键边界 -- `feeCode` 允许在编辑时修改,OpenAPI 只约束 `^[A-Za-z0-9_-]+$` 与 50 字符;不要新增“编码不可变”的前端禁用、改名拦截或唯一性推断,以服务端校验为准。 +- 服务端支持修改 `feeCode`,省略时保留原编码;当前 Admin 表单未暴露该字段,不因本指引扩展界面。编码格式、长度与唯一性以服务端契约和校验为准。 - `defaultPrice` 只是带入初值:仅当用户主动选择该项目时带入(入库同时带单价与金额、柜次带金额),**均允许人工修改**;为空表示不自动带价,`0` 表示明确的零元。修改或清空默认价不追改已有费用;打开已有费用、切换供应商或保存业务费用时不重新套用默认价,也不改变审核、发布与结算流程。 - 停用费用项目不回溯已出账费用;历史账单继续显示原项目名称快照。 - 供应商类别仅对需要供应商的费用类型有效,留空表示任意启用供应商,不由前端补默认值。 diff --git a/src/modules/inbounds/AGENTS.md b/src/modules/inbounds/AGENTS.md index 3de4e682..0f94bf45 100644 --- a/src/modules/inbounds/AGENTS.md +++ b/src/modules/inbounds/AGENTS.md @@ -4,7 +4,7 @@ - 本文件适用于 `src/modules/inbounds/`,代码路径相对本目录;应用公共规则见 `../../../AGENTS.md`,字段与计费细则见 `../../../docs/business-index.md`。 - 模块按子域分目录:`measurement/`(测量)、`review/`(测量审核)、`weight/`(称重与 OCR)、`fee/`(费用与人工定价)、`core/`(入库单公共)。先定子域再进文件。 -- 后端只查 `firstleg/`(测量、审核、计价、扣货、称重库位)与 `warehouse/`(库位);纯页面与文案不读后端。 +- 后端入口为 `firstleg/`(测量、审核、计价、扣货、称重库位)与 `warehouse/`(库位);仅涉及展示、文案且不涉及接口、权限、状态、金额或数据归属时,默认不读后端;涉及上述边界时沿实际 API 进入对应服务。 ## 页面与入口 diff --git a/src/modules/inbounds/measurement/InboundMeasurementPanel.vue b/src/modules/inbounds/measurement/InboundMeasurementPanel.vue index 8cfc349a..06353c19 100644 --- a/src/modules/inbounds/measurement/InboundMeasurementPanel.vue +++ b/src/modules/inbounds/measurement/InboundMeasurementPanel.vue @@ -885,7 +885,7 @@ defineExpose({ saveDraft, submitForReview, updateWeightEvidence, submissionBlock - +
  • {{ error }}
diff --git a/src/modules/ocean-bookings/components/OceanBookingDocumentsPanel.vue b/src/modules/ocean-bookings/components/OceanBookingDocumentsPanel.vue index 0d6a1b03..d6abc0de 100644 --- a/src/modules/ocean-bookings/components/OceanBookingDocumentsPanel.vue +++ b/src/modules/ocean-bookings/components/OceanBookingDocumentsPanel.vue @@ -4,6 +4,7 @@ import { computed, ref } from 'vue' import { FileUp } from '@lucide/vue' import { ElMessage, ElMessageBox } from 'element-plus' import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' +import InlineHelp from '@/shared/components/InlineHelp.vue' import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' import { downloadOceanBookingDocument, invalidateOceanBookingDocument, uploadOceanBookingDocument } from '../api/platformOceanBookings' import type { OceanBookingDocument, OceanBookingDocumentType, OceanBookingStatus } from '../types' @@ -109,13 +110,13 @@ async function selectFile(event: Event, type: OceanBookingDocumentType) { diff --git a/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue b/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue index 9e25fc1b..53c13beb 100644 --- a/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue +++ b/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue @@ -387,7 +387,7 @@ onMounted(() => { void load(); void loadOptions() }) diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.vue b/src/modules/unified-orders/pages/UnifiedOrdersView.vue index b512976d..fcf3c96a 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.vue +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.vue @@ -2981,7 +2981,7 @@ function orderDialogTitle(action: string, inboundCode?: string | null) { - +

所选入仓码

@@ -3019,7 +3019,7 @@ function orderDialogTitle(action: string, inboundCode?: string | null) { /> - + @@ -3028,12 +3028,12 @@ function orderDialogTitle(action: string, inboundCode?: string | null) { - + 选择文件 - +

订单审核资料

diff --git a/src/modules/warehouse-resources/locations/pages/LocationsView.test.ts b/src/modules/warehouse-resources/locations/pages/LocationsView.test.ts index df00f1a4..d48e7b32 100644 --- a/src/modules/warehouse-resources/locations/pages/LocationsView.test.ts +++ b/src/modules/warehouse-resources/locations/pages/LocationsView.test.ts @@ -162,7 +162,7 @@ describe('LocationsView', () => { it('uses the prototype-sized centered editor without a detail drawer or section cards', () => { expect(locationsSource).not.toContain('AppDrawer') expect(locationsSource).not.toContain('FormSection') - expect(locationsSource).toContain('width="min(560px, calc(100vw - 28px))"') + expect(locationsSource).toContain('width="min(880px, calc(100vw - 28px))"') expect(densitySource).toContain('.admin-density-dialog.el-dialog .el-dialog__body { min-height: 0; overflow-y: auto;') expect(locationsSource).toContain('.editor-field-grid { grid-template-columns: minmax(0, 1fr); }') }) diff --git a/src/modules/warehouse-resources/locations/pages/LocationsView.vue b/src/modules/warehouse-resources/locations/pages/LocationsView.vue index ec079b10..66b6468d 100644 --- a/src/modules/warehouse-resources/locations/pages/LocationsView.vue +++ b/src/modules/warehouse-resources/locations/pages/LocationsView.vue @@ -600,7 +600,7 @@ onMounted(() => { { } describe('approved Admin density ownership', () => { + it('keeps declaration order fields on the continuous cell-editor baseline', () => { + const source = read('modules/unified-orders/components/OrderDeclarationEditorDialog.vue') + const orderFields = source.slice(source.indexOf('') + ''.length) + expect(orderFields).toContain('class="order-fields density-form"') + expect(orderFields).toContain('density-table density-table--editable') + expect(orderFields).not.toMatch(/') + expect(orderFields).not.toContain(' { const tokenValues: Record = {} cssRoot(tokens).walkDecls(decl => { tokenValues[decl.prop] = decl.value })