From f155976f52fd4c67c5101aaff4b5c7babd335b7e Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 20 Oct 2026 16:45:09 +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 --- .../AppointmentCalendarView.test.ts | 17 ++++++++-- .../AppointmentCalendarView.vue | 4 +-- .../appointment-calendar/PROTOTYPE_MATRIX.md | 6 ++++ .../appointment-calendar/tripCalendar.test.ts | 34 ++++++++++++------- .../appointment-calendar/tripCalendar.ts | 21 ++++++++---- .../review/MeasurementReviewPanel.test.ts | 20 +++++------ .../review/MeasurementReviewPanel.vue | 2 +- src/modules/overseas-master-data/AGENTS.md | 2 ++ .../OwnedBusinessPhotosEditor.test.ts | 2 +- .../pages/OwnedBusinessViews.test.ts | 9 +++-- .../pages/DispatchDetailView.test.ts | 15 ++++++++ .../pages/DispatchDetailView.vue | 7 ++-- .../pages/ReturnInventoryDialogs.test.ts | 5 ++- .../pages/prototypeContract.test.ts | 2 +- .../suppliers/pages/SuppliersView.test.ts | 2 +- .../pages/UnifiedOrdersView.vue | 6 ++-- src/styles/adminDensity.css | 5 +++ src/styles/adminDensity.test.ts | 27 +++++++++++++++ 18 files changed, 139 insertions(+), 47 deletions(-) diff --git a/src/modules/appointment-calendar/AppointmentCalendarView.test.ts b/src/modules/appointment-calendar/AppointmentCalendarView.test.ts index 98e7c540..528f29a2 100644 --- a/src/modules/appointment-calendar/AppointmentCalendarView.test.ts +++ b/src/modules/appointment-calendar/AppointmentCalendarView.test.ts @@ -88,10 +88,10 @@ describe('simplified appointment entry', () => { wrapper.unmount() }) it('does not request unreadable domains and describes the incomplete scope', async () => { - routing.denied.add('platform:fleet:trip:list'); routing.denied.add('platform:overseas-warehouse:last-mile-order:list') + routing.denied.add('platform:overseas-warehouse:dispatch:list'); routing.denied.add('platform:overseas-warehouse:last-mile-order:list') const wrapper = renderSchedule(); await flushPromises() expect(listTripCalendar).not.toHaveBeenCalled(); expect(getWarehouseArrivals).not.toHaveBeenCalled() - expect(wrapper.text()).toContain('缺少车次读取权限') + expect(wrapper.text()).toContain('缺少排车读取权限') expect(wrapper.text()).toContain('缺少尾程订单读取权限') wrapper.unmount() }) @@ -143,4 +143,17 @@ describe('simplified appointment entry', () => { expect(routing.push).toHaveBeenCalledWith({ path: '/container-loading/overseas/1', query: { appointmentReturn: '/appointment-calendar?date=2026-10-21&mode=week' } }) wrapper.unmount() }) + it('opens the real dispatch detail with calendar context without requiring retired trip-list access', async () => { + routing.denied.add('platform:fleet:trip:list') + vi.mocked(listTripCalendar).mockResolvedValueOnce({ records: [{ sourceType: 'DISPATCH', dispatchNo: 'DS/1', status: 'PLANNED', originWarehouseId: '1', originWarehouseCode: 'US-W', originWarehouseName: '美国仓', originCountryCode: 'US', originTimezone: 'America/Los_Angeles', originWarehouseStatus: 'ACTIVE', plannedDepartureAt: '2026-10-21T16:00:00Z', expectedDriverEndAt: null, departedAt: null, completedAt: null, stops: [], incompleteReasons: [] }], unresolvedRecords: [] }) + const wrapper = renderSchedule(); await flushPromises() + const panel = wrapper.getComponent({ name: 'AppointmentCalendarPanel' }) + const event = panel.props('events').find((value: ScheduleEvent) => value.type === 'dispatch') + expect(listTripCalendar).toHaveBeenCalledOnce() + expect(event.sourcePath).toBe('/overseas-warehouse/dispatches/DS%2F1') + panel.vm.$emit('select', '2026-10-21', 'week') + panel.vm.$emit('open', event); await flushPromises() + expect(routing.push).toHaveBeenCalledWith({ path: '/overseas-warehouse/dispatches/DS%2F1', query: { appointmentReturn: '/appointment-calendar?date=2026-10-21&mode=week' } }) + wrapper.unmount() + }) }) diff --git a/src/modules/appointment-calendar/AppointmentCalendarView.vue b/src/modules/appointment-calendar/AppointmentCalendarView.vue index 3158509c..ce7483d5 100644 --- a/src/modules/appointment-calendar/AppointmentCalendarView.vue +++ b/src/modules/appointment-calendar/AppointmentCalendarView.vue @@ -65,12 +65,12 @@ async function load(next = range.value) { } catch (cause) { fail('计划到仓读取失败', cause) } })(), (async () => { - if (!access.can('platform:fleet:trip:list')) { notices.value.push('缺少车次读取权限,当前未显示排车安排。'); return } + if (!access.can('platform:overseas-warehouse:dispatch:list')) { notices.value.push('缺少排车读取权限,当前未显示排车安排。'); return } try { const result = projectTripCalendar(await listTripCalendar(next, signal), next) if (request !== generation) return trips.value = result.events - if (result.unresolved.length) notices.value.push(`${result.unresolved.length} 个车次缺少起运仓或时区资料,请到车辆管理核对。`) + if (result.unresolved.length) notices.value.push(`${result.unresolved.length} 条排车资料不完整,请到排车列表核对。`) } catch (cause) { fail('排车读取失败', cause) } })(), ]) diff --git a/src/modules/appointment-calendar/PROTOTYPE_MATRIX.md b/src/modules/appointment-calendar/PROTOTYPE_MATRIX.md index 6264dbee..9cb526df 100644 --- a/src/modules/appointment-calendar/PROTOTYPE_MATRIX.md +++ b/src/modules/appointment-calendar/PROTOTYPE_MATRIX.md @@ -1,5 +1,11 @@ # 预约工作日历逐操作矩阵 +## 2026-10-20 排车入口收敛 + +用户指出预约日历仍跳旧车次详情。排车标签改用服务端关联真实整车主单返回的排车单号,打开 `/overseas-warehouse/dispatches/:dispatchNo` 独立排车详情;返回按钮显示“返回预约日历”并保留原日期与月/周视图。无合法日历返回参数时继续走原列表或拼车登记返回逻辑,外部返回地址拒绝。读取权限与排车列表一致,不要求旧车次列表权限;独立旧车次、取消排车主单不进入活动日历,不按TR号猜DS号。原四类、地点日期和实际发车归属规则保持。自动检查与发布/真实HTTP分别记录,1920×1080、100%人工操作截图待确认。 + +自动验证:相关3文件42项、Admin全量453文件4086项、类型检查与生产构建通过;后端最终常规Maven流程完成生产/测试源码编译,`FleetTripCalendarServiceTest`及真实H2查询的`FleetTripCalendarMapperTest`共9项通过,覆盖真实排车号与内部车次号不同、独立旧车次/取消主单/取货排除、跨日期和查询上限。Gateway路由合同5项通过。均经共享`local-check.ps1`执行;早先其他自有业务在途编译错误及失效生成物尝试不作为通过证据,以最终常规Maven回归为准。接口路径复用、无网关配置或数据库结构变更,匹配发布单元为Admin与Lastleg。 + ## 2026-10-19 四类日历恢复(当前结论) 依据:用户本轮确认四类与日期规则,以及截图 `C:/Users/BY-OY/AppData/Local/Temp/codex-clipboard-840bcc07-6b54-4afa-9dc7-666d09ff04f2.png`。当前结论覆盖下文旧工作台首页/明细承载描述;原业务写入规则仍按正式业务契约。 diff --git a/src/modules/appointment-calendar/tripCalendar.test.ts b/src/modules/appointment-calendar/tripCalendar.test.ts index 5d3a9ea2..4276a4b8 100644 --- a/src/modules/appointment-calendar/tripCalendar.test.ts +++ b/src/modules/appointment-calendar/tripCalendar.test.ts @@ -3,16 +3,24 @@ import { listTripCalendar, projectTripCalendar, tripQueryWindow, type TripCalend import { http } from '@/shared/api/http' vi.mock('@/shared/api/http', () => ({ http: { get: vi.fn() }, unwrapApiResponse: (value: {data: unknown}) => value.data })) const range = { from: '2026-10-01', to: '2026-11-01' } -function trip(tripNo: string, plannedDepartureAt: string, originTimezone = 'America/Los_Angeles'): TripCalendarEntry { - return { sourceType: 'TRIP_PLAN', tripNo, status: 'PLANNED', originWarehouseId: '1', originWarehouseCode: 'US-W', originWarehouseName: 'US warehouse', originCountryCode: 'US', originTimezone, originWarehouseStatus: 'ACTIVE', plannedDepartureAt, expectedDriverEndAt: null, departedAt: null, completedAt: null, stops: [{name:'US warehouse'}, {name:'FBA'}], incompleteReasons: [] } +function trip(dispatchNo: string, plannedDepartureAt: string, originTimezone = 'America/Los_Angeles'): TripCalendarEntry { + return { sourceType: 'DISPATCH', dispatchNo, status: 'PLANNED', originWarehouseId: '1', originWarehouseCode: 'US-W', originWarehouseName: 'US warehouse', originCountryCode: 'US', originTimezone, originWarehouseStatus: 'ACTIVE', plannedDepartureAt, expectedDriverEndAt: null, departedAt: null, completedAt: null, stops: [{name:'US warehouse'}, {name:'FBA'}], incompleteReasons: [] } } describe('location-based trip calendar', () => { + it('routes the canonical dispatch number to dispatch detail and does not invent a link without it', () => { + const row = trip('DS/REAL', '2026-10-10T12:00:00Z') + const result = projectTripCalendar({ records: [row], unresolvedRecords: [] }, range) + expect(result.events[0]).toMatchObject({ key: 'dispatch-DS/REAL', title: 'DS/REAL 排车', sourcePath: '/overseas-warehouse/dispatches/DS%2FREAL' }) + const missing = projectTripCalendar({ records: [{ ...row, dispatchNo: '' }], unresolvedRecords: [] }, range) + expect(missing.events).toEqual([]) + expect(missing.unresolved[0]!.incompleteReasons).toContain('DISPATCH_REFERENCE_UNKNOWN') + }) it('counts a shared trip once and excludes cancelled arrangements', () => { - const shared = trip('TR-SHARED', '2026-10-10T12:00:00Z') - const cancelled = { ...trip('TR-CANCELLED', '2026-10-10T12:00:00Z'), status: 'CANCELLED' } + const shared = trip('DS-SHARED', '2026-10-10T12:00:00Z') + const cancelled = { ...trip('DS-CANCELLED', '2026-10-10T12:00:00Z'), status: 'CANCELLED' } const result = projectTripCalendar({ records: [shared, shared, cancelled], unresolvedRecords: [] }, range) expect(result.events).toHaveLength(1) - expect(result.events[0]).toMatchObject({ type: 'dispatch', title: 'TR-SHARED 排车' }) + expect(result.events[0]).toMatchObject({ type: 'dispatch', title: 'DS-SHARED 排车' }) }) it('queries one padded UTC window for 42 location dates without frontend page scanning', async () => { const localRange = {from:'2026-09-28',to:'2026-11-09'} @@ -25,23 +33,23 @@ describe('location-based trip calendar', () => { it('includes the first and last local day and removes padded rows outside each origin date range', () => { const records=[trip('LA-before','2026-10-01T06:30:00Z'),trip('NY-start','2026-10-01T06:30:00Z','America/New_York'),trip('LA-end','2026-11-01T06:30:00Z'),trip('NY-after','2026-11-01T06:30:00Z','America/New_York')] const projected=projectTripCalendar({records,unresolvedRecords:[]},range) - expect(projected.events.map(event=>[event.key,event.date,event.time])).toEqual([['trip-NY-start','2026-10-01','02:30'],['trip-LA-end','2026-10-31','23:30']]) + expect(projected.events.map(event=>[event.key,event.date,event.time])).toEqual([['dispatch-NY-start','2026-10-01','02:30'],['dispatch-LA-end','2026-10-31','23:30']]) }) it('does not guess a slot for absent, fixed-offset, invalid or browser-unsupported time zones', () => { const unknown={...trip('unknown','2026-10-10T12:00:00Z'),originTimezone:null,incompleteReasons:['ORIGIN_TIMEZONE_UNKNOWN']} const result=projectTripCalendar({records:[trip('offset','2026-10-10T12:00:00Z','UTC-5'),trip('bad','2026-10-10T12:00:00Z','Mars/Base'),trip('naive','2026-10-10T12:00:00')],unresolvedRecords:[unknown]},range) - expect(result.events).toEqual([]);expect(result.unresolved.map(row=>row.tripNo)).toEqual(['unknown','offset','bad','naive']) + expect(result.events).toEqual([]);expect(result.unresolved.map(row=>row.dispatchNo)).toEqual(['unknown','offset','bad','naive']) }) it('uses actual local departure through DST changes while retaining the original plan for comparison', () => { - const row={...trip('TR-DST','2026-11-01T05:30:00Z','America/New_York'),departedAt:'2026-11-02T05:00:00Z',stops:Array.from({length:20},(_,i)=>({name:`Stop ${i}`}))} - const events=projectTripCalendar({records:[row,trip('TR-DST-2','2026-11-01T06:30:00Z','America/New_York')],unresolvedRecords:[]},{from:'2026-11-01',to:'2026-11-08'}).events + const row={...trip('DS-DST','2026-11-01T05:30:00Z','America/New_York'),departedAt:'2026-11-02T05:00:00Z',stops:Array.from({length:20},(_,i)=>({name:`Stop ${i}`}))} + const events=projectTripCalendar({records:[row,trip('DS-DST-2','2026-11-01T06:30:00Z','America/New_York')],unresolvedRecords:[]},{from:'2026-11-01',to:'2026-11-08'}).events expect(events.map(event=>event.time)).toEqual(['00:00','01:30']) - expect(events[0]).toMatchObject({date:'2026-11-02',title:'TR-DST 实际发车'}) + expect(events[0]).toMatchObject({date:'2026-11-02',title:'DS-DST 实际发车'}) expect(events[0]!.fields).toContainEqual({label:'计划出发(UTC)',value:'2026-11-01T05:30:00Z'}) expect(events[0]!.fields).toContainEqual({label:'路线',value:'Stop 0 → Stop 1 → Stop 2 等20站'}) }) it('keeps executed trips in the actual month after replanning and excludes their old plan month', () => { - const executed = {...trip('TR-ACTUAL', '2026-10-31T12:00:00Z'), departedAt:'2026-11-01T12:00:00Z'} + const executed = {...trip('DS-ACTUAL', '2026-10-31T12:00:00Z'), departedAt:'2026-11-01T12:00:00Z'} expect(projectTripCalendar({records:[executed],unresolvedRecords:[]}, range).events).toEqual([]) const november = {from:'2026-11-01',to:'2026-12-01'} const event = projectTripCalendar({records:[executed],unresolvedRecords:[]}, november).events[0] @@ -50,9 +58,9 @@ describe('location-based trip calendar', () => { expect(projectTripCalendar({records:[changed],unresolvedRecords:[]},november).events[0]).toMatchObject({date:event!.date,time:event!.time}) }) it('does not fall back to the plan when an actual departure cannot be resolved', () => { - const invalid = {...trip('TR-INVALID', '2026-10-15T12:00:00Z'), departedAt:'2026-10-16T12:00:00'} + const invalid = {...trip('DS-INVALID', '2026-10-15T12:00:00Z'), departedAt:'2026-10-16T12:00:00'} const result = projectTripCalendar({records:[invalid],unresolvedRecords:[]},range) expect(result.events).toEqual([]) - expect(result.unresolved.map(row=>row.tripNo)).toEqual(['TR-INVALID']) + expect(result.unresolved.map(row=>row.dispatchNo)).toEqual(['DS-INVALID']) }) }) diff --git a/src/modules/appointment-calendar/tripCalendar.ts b/src/modules/appointment-calendar/tripCalendar.ts index 0115bf4c..afcf4b2b 100644 --- a/src/modules/appointment-calendar/tripCalendar.ts +++ b/src/modules/appointment-calendar/tripCalendar.ts @@ -2,8 +2,8 @@ import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' import type { ScheduleEvent } from './schedule' export interface TripCalendarEntry { - sourceType: 'TRIP_PLAN' - tripNo: string + sourceType: 'DISPATCH' + dispatchNo: string status: string originWarehouseId: string | null originWarehouseCode: string | null @@ -23,6 +23,7 @@ export const tripIssueLabels: Record = { ORIGIN_WAREHOUSE_UNKNOWN: '起运仓资料缺失', ORIGIN_COUNTRY_UNKNOWN: '起运国家待核实', ORIGIN_TIMEZONE_UNKNOWN: '起运仓时区待核实', ROUTE_INCOMPLETE: '路线资料不完整', LOCAL_DATE_UNRESOLVED: '无法确定当地日期', + DISPATCH_REFERENCE_UNKNOWN: '排车单号缺失', } export function tripIssues(row: TripCalendarEntry) { return row.incompleteReasons.map(reason => tripIssueLabels[reason] || '资料待核实').join(';') @@ -56,7 +57,13 @@ function localStamp(value: string | null, zone: string | null) { export function projectTripCalendar(result: TripCalendarResult, range: { from: string; to: string }) { const events: ScheduleEvent[] = [], unresolved = [...result.unresolvedRecords] const labels: Record = { PLANNED: '计划中', IN_TRANSIT: '运输中', COMPLETED: '货物已结案', CANCELLED: '已取消' } - for (const row of new Map(result.records.map(row => [row.tripNo, row])).values()) { + const seen = new Set() + for (const row of result.records) { + if (!row.dispatchNo?.trim()) { + unresolved.push({ ...row, incompleteReasons: [...row.incompleteReasons, 'DISPATCH_REFERENCE_UNKNOWN'] }); continue + } + if (seen.has(row.dispatchNo)) continue + seen.add(row.dispatchNo) if (row.status === 'CANCELLED') continue // Executed work stays on its actual local date even if the plan changes later. const occurrence = localStamp(row.departedAt ?? row.plannedDepartureAt, row.originTimezone) @@ -66,11 +73,11 @@ export function projectTripCalendar(result: TripCalendarResult, range: { from: s if (occurrence.date < range.from || occurrence.date >= range.to) continue const actual = localStamp(row.departedAt, row.originTimezone) const driverEnd = localStamp(row.expectedDriverEndAt, row.originTimezone) - events.push({ key: `trip-${row.tripNo}`, type: 'dispatch', date: occurrence.date, time: occurrence.time, - title: `${row.tripNo} ${row.departedAt ? '实际发车' : '排车'}`, subtitle: row.departedAt ? '按起运仓当地实际发车时间展示' : '海外仓送往 FBA/客户仓的车辆安排', - sourcePath: `/overseas-master-data/trips/${encodeURIComponent(row.tripNo)}`, + events.push({ key: `dispatch-${row.dispatchNo}`, type: 'dispatch', date: occurrence.date, time: occurrence.time, + title: `${row.dispatchNo} ${row.departedAt ? '实际发车' : '排车'}`, subtitle: row.departedAt ? '按起运仓当地实际发车时间展示' : '海外仓送往 FBA/客户仓的车辆安排', + sourcePath: `/overseas-warehouse/dispatches/${encodeURIComponent(row.dispatchNo)}`, fields: [ - { label: '车次状态', value: labels[row.status] || '待核实' }, + { label: '运输状态', value: labels[row.status] || '待核实' }, { label: '起运仓', value: row.originWarehouseName || row.originWarehouseCode }, { label: '地点时区', value: row.originTimezone }, { label: '计划出发(UTC)', value: row.plannedDepartureAt }, diff --git a/src/modules/inbounds/review/MeasurementReviewPanel.test.ts b/src/modules/inbounds/review/MeasurementReviewPanel.test.ts index 4a44955b..b7968f82 100644 --- a/src/modules/inbounds/review/MeasurementReviewPanel.test.ts +++ b/src/modules/inbounds/review/MeasurementReviewPanel.test.ts @@ -69,7 +69,7 @@ describe('MeasurementReviewPanel', () => { const toolbar = wrapper.get('.review-toolbar') expect(toolbar.text()).toContain('待审核') expect(toolbar.text()).toContain('修改重量') - expect(toolbar.text()).toContain('通过并进入待分柜') + expect(toolbar.findAll('button').map(button => button.text())).toContain('通过') expect(toolbar.text()).toContain('刷新') expect(toolbar.text()).toContain('客户:柏悦客户') expect(toolbar.text()).toContain('渠道:海运') @@ -99,10 +99,10 @@ describe('MeasurementReviewPanel', () => { const warning = vi.spyOn(ElMessage, 'warning') const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001', fulfillmentType: 'ONE_CLICK_DROPSHIP' }, global: { plugins: [ElementPlus] } }) await flushPromises() - expect(wrapper.text()).not.toContain('通过并进入待分柜') - await wrapper.findAll('button').find((button) => button.text().includes('通过并进入待一件代发组托'))!.trigger('click') + expect(wrapper.text()).not.toContain('通过并进入') + await wrapper.findAll('button').find((button) => button.text() === '通过')!.trigger('click') await flushPromises() - expect(confirmation).toHaveBeenCalledWith(expect.stringContaining('进入待一件代发组托'), '确认测量审核通过', expect.objectContaining({ confirmButtonText: '通过并进入待一件代发组托' })) + expect(confirmation).toHaveBeenCalledWith(expect.stringContaining('进入待一件代发组托'), '确认测量审核通过', expect.objectContaining({ confirmButtonText: '通过' })) expect(initialPricingSucceeded ? success : warning).toHaveBeenCalledWith(expect.stringContaining('入库单已进入待一件代发组托')) expect(approveMeasurement).toHaveBeenCalledTimes(1) expect(wrapper.emitted('changed')).toHaveLength(1) @@ -209,7 +209,7 @@ describe('MeasurementReviewPanel', () => { await wrapper.get('.mock-weight-change').trigger('click') await flushPromises() - await wrapper.findAll('button').find((button) => button.text().includes('通过并进入待分柜'))?.trigger('click') + await wrapper.findAll('button').find((button) => button.text() === '通过')?.trigger('click') await flushPromises() expect(getMeasurementReviewDetail).toHaveBeenCalledTimes(2) @@ -227,7 +227,7 @@ describe('MeasurementReviewPanel', () => { await flushPromises() expect(wrapper.find('.review-toolbar__actions').exists()).toBe(false) expect(wrapper.get('.review-toolbar__context').text()).toContain('客户:柏悦客户') - expect(wrapper.vm.approvalButtonLabel).toBe('通过并进入待分柜') + expect(wrapper.vm.approvalButtonLabel).toBe('通过') wrapper.vm.enableWeightCorrection() await flushPromises() expect(wrapper.getComponent({ name: 'WeightOcrPanel' }).props('correctionMode')).toBe(true) @@ -245,7 +245,7 @@ describe('MeasurementReviewPanel', () => { const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } }) await flushPromises() - const approveButton = wrapper.findAll('button').find((button) => button.text().includes('通过并进入待分柜'))! + const approveButton = wrapper.findAll('button').find((button) => button.text() === '通过')! await approveButton.trigger('click') await approveButton.trigger('click') @@ -263,12 +263,12 @@ describe('MeasurementReviewPanel', () => { const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } }) await flushPromises() - await wrapper.findAll('button').find((button) => button.text().includes('通过并进入待分柜'))!.trigger('click') + await wrapper.findAll('button').find((button) => button.text() === '通过')!.trigger('click') await flushPromises() expect(wrapper.emitted('changed')).toBeUndefined() expect(wrapper.find('.review-panel').exists()).toBe(true) - expect(wrapper.findAll('button').some((button) => button.text().includes('通过并进入待分柜'))).toBe(true) + expect(wrapper.findAll('button').some((button) => button.text() === '通过')).toBe(true) }) it('blocks approval while the order detail navigation is loading', async () => { @@ -279,7 +279,7 @@ describe('MeasurementReviewPanel', () => { }) await flushPromises() - await wrapper.findAll('button').find((button) => button.text().includes('通过并进入待分柜'))!.trigger('click') + await wrapper.findAll('button').find((button) => button.text() === '通过')!.trigger('click') expect(approveMeasurement).not.toHaveBeenCalled() }) diff --git a/src/modules/inbounds/review/MeasurementReviewPanel.vue b/src/modules/inbounds/review/MeasurementReviewPanel.vue index 969b4b2c..c90096ed 100644 --- a/src/modules/inbounds/review/MeasurementReviewPanel.vue +++ b/src/modules/inbounds/review/MeasurementReviewPanel.vue @@ -49,7 +49,7 @@ const canReview = computed(() => access.can('firstleg:inbound:measurement-review const customerId = computed(() => props.customerId || props.tenantId || '') const isPending = computed(() => detail.value?.measureReviewStatus === 'PENDING' && detail.value?.processStatus === 'MEASURE_PENDING') const approvalTargetLabel = computed(() => props.fulfillmentType === 'ONE_CLICK_DROPSHIP' ? '待一件代发组托' : '待分柜') -const approvalButtonLabel = computed(() => `通过并进入${approvalTargetLabel.value}`) +const approvalButtonLabel = computed(() => '通过') const isRejected = computed(() => detail.value?.measureReviewStatus === 'REJECTED' && detail.value?.processStatus === 'MEASURE_REJECTED') const reviewStatusLabel = computed(() => inboundStatusLabel(detail.value?.measureReviewStatus, inboundReviewStatusLabels, '未提交')) const displayCustomerName = computed(() => detail.value?.companyName?.trim() || props.customerName?.trim() || '客户未命名') diff --git a/src/modules/overseas-master-data/AGENTS.md b/src/modules/overseas-master-data/AGENTS.md index 83209a82..e3dbf962 100644 --- a/src/modules/overseas-master-data/AGENTS.md +++ b/src/modules/overseas-master-data/AGENTS.md @@ -1,5 +1,7 @@ # 海外通用资料与车辆管理 +- 自有业务:`pages/OwnedBusinessView.vue`、`OwnedBusinessDetailView.vue`及`api/ownedBusiness.ts`沿`fleet-owned-business-v1.yaml`,是原排车装车后的自有车辆运营视图,不按货源分类或另建履约流程;共同规则见`../../../../../docs/海外仓业务/自有业务首版开发契约.md`。详情三个页签,运营登记1080px居中、照片760px居中;金额/油耗单位、附加与排车版本分离,`owned-business:list/edit/upload`独立,统计沿完整计划发车日期筛选。外配页面本批不实现。 + - 运输路线地图:`components/TransportRouteDialog.vue`经`index.ts`公开导出,排车装车后、本土散板列表、本土卡车表单共用;来源字段、缺值规则和原型映射唯一见`PROTOTYPE_MATRIX.md`“运输路线地图”。仓库引用经`routeLocations.ts`复用Warehouse列表/详情解析完整地址(warehouse:list);解析失败不加载地图。仅规划路线,不接GPS、不用固定样例,不在车辆主档顶部重复放入口。 - 内部拼车需求池:`pages/CapacityOffersView.vue`、`pages/CapacityOfferDetailView.vue`、`api/capacityOffers.ts`,车次详情显式打开 `components/CapacityOfferSettingsDialog.vue` 发布余位;独立 `capacity-offer:list/manage/inquiry-*` 权限,销售只操作本人意向。流程与交互唯一契约见 `../../../../../docs/海外仓业务/内部拼车需求池首版开发契约.md`,服务端沿 `fleet-capacity-offers-v1.yaml`;首版所有货到我仓上车,未确认意向不占位,列表不逐行请求详情。 diff --git a/src/modules/overseas-master-data/components/OwnedBusinessPhotosEditor.test.ts b/src/modules/overseas-master-data/components/OwnedBusinessPhotosEditor.test.ts index 80ba6b49..972d13a6 100644 --- a/src/modules/overseas-master-data/components/OwnedBusinessPhotosEditor.test.ts +++ b/src/modules/overseas-master-data/components/OwnedBusinessPhotosEditor.test.ts @@ -29,7 +29,7 @@ describe('owned business per-slot photo save',()=>{ const wrapper=mount(OwnedBusinessPhotosEditor,{global,props:{modelValue:true,detail:ownedBusinessFixture()}});await choose(wrapper) expect(calls.upload).toHaveBeenCalledTimes(1);expect(calls.bind).toHaveBeenCalledTimes(1) expect(wrapper.text()).toContain('尚未确认绑定保存');expect(wrapper.emitted('saved')).toBeUndefined() - const original=structuredClone(calls.bind.mock.calls[0]![2]) + const original={...calls.bind.mock.calls[0]![2]} await wrapper.findAll('button').find(button=>button.text()==='核对原结果并继续绑定')!.trigger('click');await flushPromises() expect(calls.upload).toHaveBeenCalledTimes(1);expect(calls.bind).toHaveBeenCalledTimes(2) expect(calls.bind.mock.calls[1]![2]).toEqual(original);expect(original.fileId).toBe(photo.fileId);expect(original.recordVersion).toBe('0') diff --git a/src/modules/overseas-master-data/pages/OwnedBusinessViews.test.ts b/src/modules/overseas-master-data/pages/OwnedBusinessViews.test.ts index 432cd58c..5bce505c 100644 --- a/src/modules/overseas-master-data/pages/OwnedBusinessViews.test.ts +++ b/src/modules/overseas-master-data/pages/OwnedBusinessViews.test.ts @@ -18,8 +18,9 @@ const Tabs = defineComponent({ props: ['modelValue'], emits: ['update:modelValue const Pane = defineComponent({ props: ['name','label'], template: '
' }) const Query = defineComponent({ props: ['modelValue','fields'], emits: ['update:modelValue','search'], template: '
' }) const Table = defineComponent({ props: ['data','columns','total','page','pageSize'], emits: ['update:page','update:pageSize'], setup(props,{slots}) { return () => h('div', [slots['toolbar-start']?.(), slots.filters?.(), slots.notices?.(), ...(props.data || []).map((row: object) => h('div', Object.entries(slots).filter(([name]) => !['toolbar-start','filters','notices'].includes(name)).map(([,slot]) => slot?.({row}))))]) } }) -const Editor = defineComponent({ props: ['modelValue','detail'], emits: ['update:modelValue','saved','busy'], template: '
' }) -const global = { stubs: { AdminPage: Surface, DetailPageLayout: Surface, DataTable: Table, QueryToolbar: Query, ElTabs: Tabs, ElTabPane: Pane, ElButton: Button, OwnedBusinessOperationsEditor: Editor, OwnedBusinessPhotosEditor: Editor, LoadErrorState: { props:['message'], template:'
{{message}}
' }, ApiErrorMeta: true }, directives: { loading: () => {} } } +const OperationsEditor = defineComponent({ name: 'OwnedBusinessOperationsEditorStub', props: ['modelValue','detail'], emits: ['update:modelValue','saved','busy'], template: '
' }) +const PhotosEditor = defineComponent({ name: 'OwnedBusinessPhotosEditorStub', props: ['modelValue','detail'], emits: ['update:modelValue','saved','busy'], template: '
' }) +const global = { stubs: { AdminPage: Surface, DetailPageLayout: Surface, DataTable: Table, QueryToolbar: Query, ElTabs: Tabs, ElTabPane: Pane, ElButton: Button, OwnedBusinessOperationsEditor: OperationsEditor, OwnedBusinessPhotosEditor: PhotosEditor, LoadErrorState: { props:['message'], template:'
{{message}}
' }, ApiErrorMeta: true }, directives: { loading: () => {} } } beforeEach(() => { vi.clearAllMocks(); calls.allowed = new Set(Object.values(permission)); calls.allowed.add('platform:warehouse:list') @@ -49,9 +50,11 @@ describe('owned business views', () => { }) it('opens the operations modal on demand with auxiliary zero version instead of the dispatch version', async () => { const wrapper=mount(OwnedBusinessView,{global});await flushPromises() + expect(calls.detail).not.toHaveBeenCalled();expect(wrapper.findComponent(OperationsEditor).props('modelValue')).toBe(false) await wrapper.findAll('button').find(button=>button.text()==='登记运营信息')!.trigger('click');await flushPromises() expect(calls.detail).toHaveBeenCalledExactlyOnceWith('DS-OWN-1') - const editor=wrapper.findAllComponents(Editor).find(child=>child.props('modelValue'))! + const editor=wrapper.findComponent(OperationsEditor) + expect(editor.props('modelValue')).toBe(true);expect(wrapper.findComponent(PhotosEditor).props('modelValue')).toBe(false) expect(editor.props('detail').row.recordVersion).toBe('0');expect(editor.props('detail').dispatch.recordVersion).toBe('8') wrapper.unmount() }) diff --git a/src/modules/overseas-warehouse/pages/DispatchDetailView.test.ts b/src/modules/overseas-warehouse/pages/DispatchDetailView.test.ts index a604054a..09f3121e 100644 --- a/src/modules/overseas-warehouse/pages/DispatchDetailView.test.ts +++ b/src/modules/overseas-warehouse/pages/DispatchDetailView.test.ts @@ -19,6 +19,21 @@ vi.mock('@/modules/overseas-master-data', async () => ({ const options = { global: { plugins: [ElementPlus], stubs: { DataTable: DataTableStub, DispatchWorkflowDialog: true } } } beforeEach(() => { vi.clearAllMocks(); vi.mocked(getDispatch).mockResolvedValue(detail()); can.mockReturnValue(false); push.mockReset(); Object.keys(query).forEach(key => delete query[key]) }) describe('whole-dispatch four-tab detail', () => { + it('returns to the original appointment date and view from the dispatch detail', async () => { + query.appointmentReturn = '/appointment-calendar?date=2026-10-21&mode=week' + const wrapper = mount(DispatchDetailView, options); await flushPromises() + expect(wrapper.get('button.density-back').text()).toContain('返回预约日历') + await wrapper.get('button.density-back').trigger('click') + expect(push).toHaveBeenCalledWith('/appointment-calendar?date=2026-10-21&mode=week') + wrapper.unmount() + }) + it('ignores an external calendar return URL and keeps the normal dispatch-list return', async () => { + query.appointmentReturn = 'https://outside.invalid/appointment-calendar?date=2026-10-21' + const wrapper = mount(DispatchDetailView, options); await flushPromises() + await wrapper.get('button.density-back').trigger('click') + expect(push).toHaveBeenCalledWith({ name: 'overseas-dispatches' }) + wrapper.unmount() + }) it('uses only four accessible colored tabs and switches without another network read', async () => { const value = detail(); value.dispatchInfo.fbaAppointments = [{ batchKey: 'FBA_APPOINTMENT:1', batchNo: 'BATCH-1', appointmentNo: 'FBA-AP-1', destinationKey: 'FBA:ONT8', destinationWarehouseCode: 'ONT8', status: 'CONFIRMED', confirmedAt: null, blockReason: null }] vi.mocked(getDispatch).mockResolvedValueOnce(value) diff --git a/src/modules/overseas-warehouse/pages/DispatchDetailView.vue b/src/modules/overseas-warehouse/pages/DispatchDetailView.vue index 6bf14f9b..52ee2267 100644 --- a/src/modules/overseas-warehouse/pages/DispatchDetailView.vue +++ b/src/modules/overseas-warehouse/pages/DispatchDetailView.vue @@ -3,6 +3,7 @@ import '@/styles/adminDensity.css' import { computed, onBeforeUnmount, ref, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' import { inquiryReturnTarget, vehicleStatusLabels } from '@/modules/overseas-master-data' +import { appointmentReturnTarget } from '@/modules/appointment-calendar' import { useAccessPreviewStore } from '@/core/access/accessPreview' import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' @@ -65,7 +66,9 @@ async function load() { catch (error) { if (current === generation) loadError.value = getApiErrorDetails(error, '排车详情加载失败,请重试') } finally { if (current === generation) loading.value = false } } -function back() { void router.push(inquiryReturnTarget(route.query) || { name: 'overseas-dispatches' }) } +const appointmentReturn = computed(() => appointmentReturnTarget(route.query)) +const backLabel = computed(() => appointmentReturn.value ? '返回预约日历' : inquiryReturnTarget(route.query) ? '返回原拼车登记' : '返回排车列表') +function back() { void router.push(appointmentReturn.value || inquiryReturnTarget(route.query) || { name: 'overseas-dispatches' }) } function openAction(value: DispatchAction, requestedOperation?: 'ROUTE') { if (!canDispatchAction(detail.value?.actions, value, access.can)) return; rejectionError.value = null; operation.value = requestedOperation; action.value = value; actionOpen.value = true } async function handleRejection(details: ApiErrorDetails) { rejectionError.value = details; await load() } async function handleSaved() { rejectionError.value = null; await load() } @@ -74,7 +77,7 @@ onBeforeUnmount(() => { generation++ })