From e6c864a2a64e44e0b02ee97288c6ed47074150f2 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 21 Oct 2026 15:10:54 +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 --- src/modules/overseas-warehouse/AGENTS.md | 2 ++ .../overseas-warehouse/PROTOTYPE_MATRIX.md | 21 +++++++++-- .../components/DispatchEvidenceFiles.test.ts | 9 +++++ .../components/DispatchEvidenceFiles.vue | 19 +++++----- .../DispatchWorkflowDialog.layout.test.ts | 32 ++++++++++++++++- .../components/DispatchWorkflowDialog.vue | 20 +++++++---- .../SmartDispatchCargoBoard.test.ts | 5 +-- .../components/SmartDispatchCargoBoard.vue | 15 ++++---- .../SmartDispatchDialog.layout.test.ts | 35 ++++++++++++++++++- .../SmartDispatchDialog.planning.test.ts | 2 +- .../components/SmartDispatchDialog.test.ts | 3 +- .../components/SmartDispatchDialog.vue | 14 +++----- .../SmartDispatchExistingCargo.test.ts | 26 +++++++++++--- .../components/SmartDispatchExistingCargo.vue | 22 ++++++------ .../overseas-warehouse/dispatchContracts.ts | 7 ++-- .../dispatchPresentation.ts | 18 +++++++++- .../pages/DispatchDetailView.test.ts | 13 +++++++ .../pages/DispatchDetailView.vue | 5 +-- .../pages/DispatchesView.test.ts | 20 +++++++++-- .../pages/DispatchesView.vue | 10 ++++-- .../pages/InboundDialogs.behavior.test.ts | 5 +-- .../pages/InboundOrdersView.vue | 2 +- src/styles/adminDensity.test.ts | 6 ++++ 23 files changed, 239 insertions(+), 72 deletions(-) diff --git a/src/modules/overseas-warehouse/AGENTS.md b/src/modules/overseas-warehouse/AGENTS.md index 250d2734..2ac9e69d 100644 --- a/src/modules/overseas-warehouse/AGENTS.md +++ b/src/modules/overseas-warehouse/AGENTS.md @@ -44,6 +44,8 @@ ## 验证 +- 排车调度/理货审核分区仅显示审核、详情;审核弹框的车辆图片、司机照片和驾驶证照片通过既有本车附件接口上传,随 `ReviewCommand.attachments` 保存,不调用资源编辑命令。已有资源与审核附件只读预览,详情合并展示;当前审核权限与服务端 Gate 控制上传及提交,列表查看权限控制读取。 + - 一件代发出库候选选择:`pages/OutboundsView.vue` 按 `inventoryPositionId + skuCode` 保留跨页选择,只提供本页全选;查询/重置回到第一页,切换海外仓清空选择。提交仍校验同客户、同海外仓,数量取所选行的 `availablePackages`(箱数),不能按 SKU 件数替代;候选同时展示当前可出箱数对应的毛重、材积重和体积,创建出库单时服务端按本次出库箱数占实际入库箱数的比例冻结三项测量快照。候选交互回归见 `pages/OutboundsView.selection.test.ts`;状态展示与取消门禁见 `pages/OutboundsView.test.ts`、`pages/OutboundDetailView.test.ts`。 - 转运审核定向测试:从应用根运行 `pnpm test src/modules/overseas-warehouse/api/warehouseTransfers.test.ts src/modules/overseas-warehouse/pages/prototypeContract.test.ts`,分别核对客户作用域/API 与审核入口权限、状态。 - 装车完成通过`completeDispatchLoading`一次提交,结果仅接受`DELIVERING`;原请求恢复不重建单据。未知费用保持未确认,由既有后台费用管理维护,不按金额推断覆盖或重复记费。已有排车加货和移货仅从`SmartDispatchDialog.vue`内的`SmartDispatchExistingCargo.vue`进入;移货必须使用`GET items`返回的真实dispatchItemId及逐项remove Gate;路线仅重排`GET route`返回的既有全部目的地,包含外接点,不能新增或猜测目的地。附件沿`DispatchEvidenceFiles.vue`的本车鉴权上传/读取,上传与主单引用保存分开。 diff --git a/src/modules/overseas-warehouse/PROTOTYPE_MATRIX.md b/src/modules/overseas-warehouse/PROTOTYPE_MATRIX.md index 80a17ae4..78acca80 100644 --- a/src/modules/overseas-warehouse/PROTOTYPE_MATRIX.md +++ b/src/modules/overseas-warehouse/PROTOTYPE_MATRIX.md @@ -6,9 +6,14 @@ | 页面/操作 | 依据与问题 | 修复目标 | 验收 | |---|---|---|---| -| 新建排车/查询与设置 | 原型 `modal-dispatch-schedule` 查询在三栏之前,现状设置占据顶部 | 起运仓进入首个查询面板;建单方式、草稿恢复按需展开;保留区域按钮与现行真实字段 | 待验证 | -| 两模式/选订单、托盘与装车 | 截图中中栏固定偏移、已有货物栏替换整个布局 | 三栏共享标题行和内容行,右栏插槽只更换标题与内容,不再自行定位;长标题换行一起撑高标题行 | 待验证 | -| 已有排车/查询、加货与移除 | 两行裸露查询,货物查询被重复单号推偏 | 同一个查询面板、统一标签和输入宽度;单号及仓库移入右栏标题;保留即时保存与移除确认 | 待验证 | +| 新建排车/查询与设置 | 原型 `modal-dispatch-schedule` 查询在三栏之前,现状设置占据顶部 | 起运仓进入首个查询面板;建单方式、草稿恢复按需展开;保留区域按钮与现行真实字段 | 定向179项通过;待人工截图确认 | +| 两模式/选订单、托盘与装车 | 截图中中栏固定偏移、已有货物栏替换整个布局 | 三栏共享标题行和内容行,右栏插槽只更换标题与内容,不再自行定位;长标题换行一起撑高标题行 | 定向179项通过;待人工截图确认 | +| 已有排车/查询、加货与移除 | 两行裸露查询,货物查询被重复单号推偏 | 同一个查询面板、统一标签和输入宽度;单号及仓库移入右栏标题;保留即时保存与移除确认 | 定向179项通过;待人工截图确认 | +| 新建排车/搜索标签与分页 | 最新截图 `codex-clipboard-06b92e79-d28a-4eec-8d63-f0f856d1f380.png` 三栏已对齐,但起运仓换行、分页提示多余;用户要求仅做样式定向检查 | 按最新确认删除起运仓可见标签并默认首个可用仓;删除分页提示及对应说明入口;分页与订单表左边缘对齐 | 后续合并定向161项通过;待截图确认 | + +本轮追加:用户要求打开时默认第一个起运仓并删除标签;无已有选择、草稿或订单指定仓时采用目录首项并读取候选,空目录保持空值。已有排车按新截图 `codex-clipboard-43654ee8-6c76-4141-bca5-4d108d45b828.png` 合并为可搜索单号的选择器,分页进入下拉底部、货物查询同排,删除重复标签和介绍文字。沿用当前查询API、明确选择后读取清单,不自动选择已有排车。6个文件161项定向回归通过(18.32秒),覆盖默认仓自动加载、空目录、草稿仓保留、下拉搜索分页及原有加货/移除;本轮不跑全量测试。应用类型检查与差异检查通过,真实样式待截图确认。 + +自动检查:7个文件179项定向回归通过;`pnpm build`(含应用类型检查)通过。标题共享网格行,三栏内容共同起始,不再使用中栏固定顶部偏移;已有排车自定义内容仍留在同一网格列内。本轮修改的4个测试文件及其组件依赖定向类型检查通过;新增查询断言修正后已有排车23项再次通过。额外全仓`typecheck:all`触及统一入口2GB堆上限,未提高资源限制;扩大到未改动的规划测试时存在既有`VueWrapper`类型报错,未据此宣称全仓测试代码类型检查通过。全量`pnpm test`通过:470文件、4336项(14:34启动,591.74秒)。验证期间三个生产组件哈希未变;测试断言查询方法的最后修正已另跑23项通过。差异空白检查通过。只调整本模块模板和布局,保留蓝白配色及现行排车契约;未部署,视觉验收待1920×1080、100%人工截图确认。 ## 2026-10-21 智能排车截图复核与样式纠偏 @@ -390,3 +395,13 @@ | 预约列表 | 新增 / 编辑 FBA 预约 | 用户截图 Image 2 与现有 WorkCreateAppointmentForm / WorkAppointmentForm 居中弹框 | 共用逐票资料补充;已有安排明确指向原预约编辑,保留原占用规则 | 定向回归通过;待人工截图确认 | 自动验证:本轮 Admin 全量初跑 469 文件、4333 项,4315 项通过;2 个关联文件的 18 项失败在修复后,由包含这两文件的 45 文件、426 项定向回归全部通过。最终补充文案与表单另行复测 11 项通过;最终 `pnpm build`(含应用类型检查)通过。未触发部署、未写测试环境数据;真实页面仍待人工截图确认。 + +### 2026-10-21 排车审核入口与附件 + +| 页面 | 操作 | 依据与承载 | 本次变更 | 验收 | +|---|---|---|---|---| +| 排车列表:调度审核、理货员审核 | 行操作 | 用户本轮确认只保留审核、详情;沿现有列表 | 去掉维护车辆信息及更多菜单,仅显示本环节审核与详情 | 本轮定向回归通过;待人工截图确认 | +| 排车审核弹框 | 通过/驳回、上传附件 | 用户附件 codex-clipboard-249614d2-efac-4494-af47-090980981900.png;沿既有弹框 | 车辆图片、司机照片、驾驶证照片三类,原证据只读,本次附件随审核结果保存;未知结果重试原请求 | 本轮定向回归通过;待人工 1920×1080、100% 截图确认 | +| 排车详情:车辆信息 | 查看审核附件 | 复用既有附件预览 | 汇总原资源与两级审核已保存证据,去重并只读预览 | 本轮定向回归通过;待人工截图确认 | + +本轮自动验证:Admin 5 个定向文件 96 项通过,`pnpm typecheck` 通过;Lastleg/File 8 个定向类 63 项通过,API 路由合同 5 项通过,AGENTS 索引 1232 项通过。未部署,未进行真实页面操作;上线本次变更须包含 Admin、lastleg-service 和 file(新增司机照片文件分类)。 diff --git a/src/modules/overseas-warehouse/components/DispatchEvidenceFiles.test.ts b/src/modules/overseas-warehouse/components/DispatchEvidenceFiles.test.ts index 02d13fe8..caef2797 100644 --- a/src/modules/overseas-warehouse/components/DispatchEvidenceFiles.test.ts +++ b/src/modules/overseas-warehouse/components/DispatchEvidenceFiles.test.ts @@ -12,6 +12,15 @@ const options = { props: { modelValue: ['301'], dispatchNo: 'PC/1', kind: 'VEHIC const button = (wrapper: ReturnType, label: string) => wrapper.findAll('button').find(value => value.text() === label)! async function selectFile(wrapper: ReturnType, file: File) { const input = wrapper.get('input[type="file"]'); Object.defineProperty(input.element, 'files', { configurable: true, value: [file] }); await input.trigger('change'); await flushPromises() } beforeEach(() => { vi.resetAllMocks(); vi.mocked(api.getDispatchEvidence).mockResolvedValue(metadata()); vi.mocked(api.uploadDispatchEvidence).mockResolvedValue(metadata('302')); vi.mocked(api.getDispatchEvidenceContent).mockResolvedValue(new Blob(['png'], { type: 'image/png' })); URL.createObjectURL = vi.fn().mockReturnValue('blob:authenticated'); URL.revokeObjectURL = vi.fn() }) +it('keeps previously provided references read-only and only submits newly uploaded audit files', async () => { + const wrapper = mount(DispatchEvidenceFiles, { ...options, props: { ...options.props, modelValue: [], referenceIds: ['301'], kind: 'DRIVER_PHOTO', label: '司机照片' } }); await flushPromises() + expect(wrapper.findAll('tbody button').map(item => item.text())).toEqual(['预览']) + await selectFile(wrapper, new File(['pdf'], 'photo.pdf', { type: 'application/pdf' })); expect(api.uploadDispatchEvidence).not.toHaveBeenCalled() + await selectFile(wrapper, new File(['png'], 'photo.png', { type: 'image/png' })) + expect(api.uploadDispatchEvidence).toHaveBeenCalledWith('PC/1', 'DRIVER_PHOTO', expect.any(File)) + expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([['302']]) + wrapper.unmount() +}) it('previews and replaces real string-size metadata through the JSON parser and API boundary', async () => { const actual = await vi.importActual('../api/dispatches') const envelope = (fileId: string) => parseApiJsonResponse(JSON.stringify({ code: '200', data: { ...metadata(fileId), sizeBytes: '3' } })) diff --git a/src/modules/overseas-warehouse/components/DispatchEvidenceFiles.vue b/src/modules/overseas-warehouse/components/DispatchEvidenceFiles.vue index cea45f23..73a71505 100644 --- a/src/modules/overseas-warehouse/components/DispatchEvidenceFiles.vue +++ b/src/modules/overseas-warehouse/components/DispatchEvidenceFiles.vue @@ -10,13 +10,16 @@ import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' import { ApiResponseError, getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' import { getDispatchEvidence, getDispatchEvidenceContent, uploadDispatchEvidence } from '../api/dispatches' import { DispatchInputError, type DispatchEvidenceFile, type DispatchEvidenceKind } from '../dispatchContracts' -const props = defineProps<{ dispatchNo: string; kind: DispatchEvidenceKind; disabled?: boolean; readonly?: boolean }>() +const props = defineProps<{ dispatchNo: string; kind: DispatchEvidenceKind; disabled?: boolean; readonly?: boolean; label?: string; saveHint?: string; referenceIds?: string[] }>() const ids = defineModel({ required: true }) +const allIds = computed(() => [...new Set([...(props.referenceIds || []), ...ids.value])]) const emit = defineEmits<{ busy: [value: boolean] }>() const metadata = ref>({}), error = ref(null), uploading = ref(false), input = ref(null) const imageOpen = ref(false), imageUrl = ref(''), imageName = ref(''), previewLoading = ref(false) -const label = computed(() => props.kind === 'VEHICLE' ? '本趟车辆证据' : '本趟司机证照') +const label = computed(() => props.label || (props.kind === 'VEHICLE' ? '本趟车辆证据' : props.kind === 'DRIVER_PHOTO' ? '司机照片' : '本趟司机证照')) const types = ['application/pdf', 'image/jpeg', 'image/png'], maximum = 10 * 1024 * 1024, urls = new Set() +const uploadTypes = computed(() => props.kind === 'DRIVER_PHOTO' ? types.slice(1) : types) +const uploadFormats = computed(() => props.kind === 'DRIVER_PHOTO' ? 'JPG、PNG' : 'PDF、JPG、PNG') let generation = 0, previewGeneration = 0, replacing: string | null = null, pendingPopup: Window | null = null function valid(value: DispatchEvidenceFile, expected?: string) { if (!value || !/^[1-9]\d*$/.test(String(value.fileId)) || typeof value.fileId === 'number' && !Number.isSafeInteger(value.fileId) || expected && String(value.fileId) !== expected || !value.fileName?.trim() || !types.includes(value.contentType) || !Number.isSafeInteger(value.sizeBytes) || value.sizeBytes <= 0 || value.sizeBytes > maximum) throw new DispatchInputError('附件元数据未确认本车有效文件,请重新读取') @@ -25,7 +28,7 @@ function valid(value: DispatchEvidenceFile, expected?: string) { function fail(cause: unknown, fallback: string) { error.value = getApiErrorDetails(cause, cause instanceof DispatchInputError ? cause.message : fallback) } function closePreview() { previewGeneration++; pendingPopup?.close(); pendingPopup = null; imageOpen.value = false; previewLoading.value = false; if (imageUrl.value) { URL.revokeObjectURL(imageUrl.value); urls.delete(imageUrl.value); imageUrl.value = '' } } async function load() { - const current = ++generation, no = props.dispatchNo, kind = props.kind, values = [...ids.value] + const current = ++generation, no = props.dispatchNo, kind = props.kind, values = [...allIds.value] closePreview(); metadata.value = {}; error.value = null if (!no) return const results = await Promise.allSettled(values.map(async id => valid(await getDispatchEvidence(no, kind, id), id))) @@ -37,7 +40,7 @@ async function upload(event: Event) { const element = event.target as HTMLInputElement, file = element.files?.[0]; element.value = '' if (!file || props.readonly || props.disabled || uploading.value) return error.value = null - if (!types.includes(file.type) || file.size <= 0 || file.size > maximum) { fail(new DispatchInputError('请选择10MiB以内的PDF、JPG或PNG文件'), '附件不符合要求'); return } + if (!uploadTypes.value.includes(file.type) || file.size <= 0 || file.size > maximum) { fail(new DispatchInputError(`请选择10MiB以内的${uploadFormats.value}文件`), '附件不符合要求'); return } const current = generation, no = props.dispatchNo, kind = props.kind, replaceId = replacing uploading.value = true; emit('busy', true) try { @@ -71,15 +74,15 @@ async function preview(id: string) { } catch (cause) { if (current === previewGeneration) { closePreview(); fail(cause, '本车附件预览失败,原引用已保留') } } finally { if (current === previewGeneration) previewLoading.value = false } } -watch(() => JSON.stringify([props.dispatchNo, props.kind, ids.value]), load, { immediate: true }) +watch(() => JSON.stringify([props.dispatchNo, props.kind, allIds.value]), load, { immediate: true }) onBeforeUnmount(() => { generation++; closePreview(); for (const url of urls) URL.revokeObjectURL(url); urls.clear(); emit('busy', false) }) -