From 5a46198bf0d8923e1cfad64ecc3844472287d35f Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 19 Oct 2026 17:02:27 +0800 Subject: [PATCH 1/4] feat(appointments): add FBA creation and destination filtering --- .../AppointmentCalendarPanel.test.ts | 13 +- .../AppointmentCalendarPanel.vue | 8 +- .../AppointmentCalendarView.test.ts | 29 +++- .../AppointmentCalendarView.vue | 10 +- .../AppointmentCreatePanel.test.ts | 131 ++++++++++++++++ .../AppointmentCreatePanel.vue | 145 ++++++++++++++++++ .../appointment-calendar/PROTOTYPE_MATRIX.md | 13 +- .../WorkCalendarPanel.test.ts | 2 +- .../WorkCreateAppointmentForm.vue | 6 +- .../appointment-calendar/calendar.test.ts | 17 +- src/modules/appointment-calendar/calendar.ts | 11 +- .../appointment-calendar/planningApi.test.ts | 11 +- .../appointment-calendar/planningApi.ts | 8 +- src/styles/adminDensity.test.ts | 3 +- 14 files changed, 382 insertions(+), 25 deletions(-) create mode 100644 src/modules/appointment-calendar/AppointmentCreatePanel.test.ts create mode 100644 src/modules/appointment-calendar/AppointmentCreatePanel.vue diff --git a/src/modules/appointment-calendar/AppointmentCalendarPanel.test.ts b/src/modules/appointment-calendar/AppointmentCalendarPanel.test.ts index 38fc7982..3ca8267c 100644 --- a/src/modules/appointment-calendar/AppointmentCalendarPanel.test.ts +++ b/src/modules/appointment-calendar/AppointmentCalendarPanel.test.ts @@ -13,7 +13,9 @@ describe('four-type appointment calendar', () => { expect(wrapper.get('[aria-label="安排类型"]').text()).toBe('到港排车还柜计划到仓') expect(wrapper.findAll('.detail-event')).toHaveLength(4) expect(wrapper.get('[aria-label="当天安排"] header').text()).toContain('4 项安排') - expect(wrapper.findAll('.calendar-day')).toHaveLength(42) + expect(wrapper.findAll('.calendar-day')).toHaveLength(35) + expect(wrapper.find('[data-date="2026-11-01"]').exists()).toBe(true) + expect(wrapper.find('[data-date="2026-11-02"]').exists()).toBe(false) expect(wrapper.text()).not.toMatch(/新增预约|全局待办|需复核|中转|直送/) }) it('filters locally without requesting another range', async () => { @@ -24,6 +26,15 @@ describe('four-type appointment calendar', () => { expect(wrapper.text()).toContain('当天暂无安排') expect(wrapper.emitted('range')).toBeUndefined() }) + it('offers creation in the header only when authorized', async () => { + const wrapper = render() + expect(wrapper.findAll('button').some(button => button.text() === '新增预约')).toBe(false) + await wrapper.setProps({ canCreate: true }) + await wrapper.findAll('.calendar-controls button').find(button => button.text() === '新增预约')!.trigger('click') + expect(wrapper.emitted('create')).toEqual([[]]) + expect(wrapper.emitted('range')).toBeUndefined() + wrapper.unmount() + }) it('changes the selected date locally and requests one range per mode or period change', async () => { const wrapper = render() await wrapper.get('[data-date="2026-10-20"]').trigger('click') diff --git a/src/modules/appointment-calendar/AppointmentCalendarPanel.vue b/src/modules/appointment-calendar/AppointmentCalendarPanel.vue index fd4bc039..cb9cc19d 100644 --- a/src/modules/appointment-calendar/AppointmentCalendarPanel.vue +++ b/src/modules/appointment-calendar/AppointmentCalendarPanel.vue @@ -1,12 +1,13 @@ + diff --git a/src/modules/appointment-calendar/PROTOTYPE_MATRIX.md b/src/modules/appointment-calendar/PROTOTYPE_MATRIX.md index 43202134..27b6b138 100644 --- a/src/modules/appointment-calendar/PROTOTYPE_MATRIX.md +++ b/src/modules/appointment-calendar/PROTOTYPE_MATRIX.md @@ -7,12 +7,23 @@ | 页面 | 操作 | 当前确认承载与布局 | 实现结论 | |---|---|---|---| | 预约列表 | 月/周、四类选择、当前范围搜索 | 左日历右当天安排;日期格简洁标签,最多三条 | 恢复 `AppointmentCalendarPanel`,不再打开全局工作明细弹框;待人工截图确认 | +| 预约列表 | 月视图周数 | 用户截图 `codex-clipboard-372a5dc5-0c82-4298-93db-089b6c97c8ca.png`;周一至周日,只覆盖当月所需完整周 | 4/5/6周自适应;2026年10月五周,保留末周11月1日,不追加11月2—8日整周;待人工截图确认 | | 预约列表 | 查看当天、未定日期 | 固定右侧区域,完整业务字段与滚动明细 | 类型仅到港、排车、还柜、计划到仓;待人工截图确认 | -| 预约列表 | 新增、审核、删除 | 本轮明确取消通用入口 | 来源业务生成记录;不在日历另建预约或改状态 | +| 预约列表 | 新增预约 | 用户本轮要求恢复顶部按钮,随后确认先支持FBA;1080px居中弹框,先选择已有货物,再登记预约时间 | 复用真实来源预览及预约保存流程,原权限/幂等/未保存保护;保存成功显示预约编号和只读内容;普通客户仓不在本次范围;待人工截图确认 | +| 新增FBA送仓预约 | 按目的仓查询 | 用户截图 `codex-clipboard-44b338fe-af49-45e2-abf2-3bc0bd27224a.png`;在原查询栏增加可搜索、可清空的目的FBA仓下拉框 | 选项使用同货物查询权限的权威美国FBA目录;提交查询后从第一页按目的仓过滤,翻页沿用条件;待人工截图确认 | +| 预约列表 | 审核、删除 | 不恢复通用入口 | 在原业务处理,不在日历改业务状态 | | 计划到仓 | 调整 | 原居中弹框保留,小型时间表单 | 整柜原命令、原权限/版本/幂等与计划冻结门禁;待人工截图确认 | | 到港、排车、还柜 | 查看与办理 | 跳原货柜/车次正式详情页 | 保留日期/月周返回上下文,无独立工作流程导航 | | 原业务深链 | 精确来源/事项办理 | 仍使用既有居中原业务表单 | 只承接其他模块实际使用的精确入口,不恢复全局工作待办;待人工截图确认 | +2026-10-19 目的FBA仓筛选补修:原查询栏增加可搜索、可清空的下拉框,首次展开读取完整权威美国FBA目录;复用原货物读取权限,不新增仓库管理权限。查询回第一页,翻页沿用目的仓条件,重置清除条件;选项失败独立提示并可重试,不隐藏已加载货物或伪装为空结果。预约模块37文件/276项及生产构建(含类型检查)通过,Gateway路由合同5项通过。后端生产源码编译通过,本次两个测试文件单独javac编译并经Surefire执行13项通过;常规Maven流程仍被派送模块三份测试中的5项编译错误阻挡,不能据定向结果宣称后端完整门禁通过。Admin全量405文件/3388项通过、1项输入提示合同失败,原因是其他在途运输单表单的7个输入框缺少placeholder;单独复查相同失败,未修改其他模块。日志为`.logs/appointment-destination-{test,build,full-test,placeholder-check}.log`,后端为同名前缀的`route`、`backend`、`javac`及`targeted-backend`日志。本补修未提交或部署,人工1920×1080/100%视觉待确认。 + +2026-10-19 新增FBA预约入口补修:顶部按钮要求原FBA预约写权限及尾程订单列表权限,点击后才读取候选;支持同起运仓、同目的仓的跨客户货物选择,核对最新真实来源后复用原创建表单。未知结果保留原请求并阻止关闭/返回,成功后移除创建表单、显示真实预约编号和只读内容,刷新原日历;不生成虚拟车次或第五类日历。本次已明确普通客户仓暂不加入。 + +验证:新增入口直接5文件/59项通过;`pnpm build`含类型检查通过,仅既有AnalyticsChart大分块提示。工作区首次全量为405文件/3385项通过、1项排车详情布局合同失败,后续该项单独6项复查通过;再次全量出现9文件/11项失败(以5秒超时为主),在不修改超时阈值和其他业务代码的情况下,以2个工作进程复查整个预约模块、密度及上述全部失败文件,47文件/385项全过。不将这些分次结果记录为单次全量绿灯。证据为`.logs/appointment-create-{focused-final,build,full-test,full-test-final,layout-recheck,bounded-recheck}.log`。额外临时候选核对未作为通过证据;工作区其他业务在途修改保留。本次未提交、发布或新增真实业务记录,人工1920×1080/100%视觉待确认。 + +2026-10-19 月视图周数补修:按月实际周数生成日期格,并同步查询开区间;覆盖2026年10月五周、跨年五周、完整二月四周与确需六周的八月。Admin全量405文件/3375项、类型检查及生产构建通过,`git diff --check`通过;仅既有AnalyticsChart大分块提示。本补修尚未提交或发布,人工视觉待确认;证据为`.logs/appointment-calendar-month-weeks-{test,build}.log`。 + 2026-10-19 发布候选检查:Admin全量405文件/3372项通过,定向输入提示、弹框密度及取货组件4文件/35项通过;`pnpm build`(含类型检查)通过,仅既有AnalyticsChart大分块提示。新增日历后端直接测试及受影响SQL、API、权限合同通过,Gateway路由/部署拓扑/回滚合同与Mapper注册合同通过。 发布检查中发现的既有问题已在本候选最小修正:调拨新增箱数和取货输入提示、取货弹框密度、尾程贴标文件页签结构与对应断言;取货测试Bean及API数量断言;国内空库SQL中误放的五张海外取货表归回海外空库基线,并同步现行车次用途字段和基线表数。这些仅修改源码基线,不对运行环境执行SQL。 diff --git a/src/modules/appointment-calendar/WorkCalendarPanel.test.ts b/src/modules/appointment-calendar/WorkCalendarPanel.test.ts index 1ecf1057..1bdb83c2 100644 --- a/src/modules/appointment-calendar/WorkCalendarPanel.test.ts +++ b/src/modules/appointment-calendar/WorkCalendarPanel.test.ts @@ -57,7 +57,7 @@ it('shows the actual cross-month interval and hides empty-day zeros while keepin }) it('renders server type counts instead of individual event cards and opens exact date/type details', async () => { const wrapper = render() - expect(wrapper.findAll('.work-day')).toHaveLength(42) + expect(wrapper.findAll('.work-day')).toHaveLength(35) const day = wrapper.get('[data-date="2026-10-15"]') expect(day.get('.day-heading').text()).toContain('2 项') await day.get('[data-type="DELIVERY"]').trigger('click') diff --git a/src/modules/appointment-calendar/WorkCreateAppointmentForm.vue b/src/modules/appointment-calendar/WorkCreateAppointmentForm.vue index beef2652..9ad0cf9c 100644 --- a/src/modules/appointment-calendar/WorkCreateAppointmentForm.vue +++ b/src/modules/appointment-calendar/WorkCreateAppointmentForm.vue @@ -2,7 +2,7 @@ import { computed, onMounted, reactive, ref } from 'vue' import { ElMessage } from 'element-plus' import { useAccessPreviewStore } from '@/core/access/accessPreview' -import { FbaAppointmentFields, createAppointmentForm, appointmentFormError, appointmentPayload, overseasWarehousePermissions } from '@/modules/overseas-warehouse' +import { FbaAppointmentFields, createAppointmentForm, appointmentFormError, appointmentPayload, overseasWarehousePermissions, type FbaAppointmentDto } from '@/modules/overseas-warehouse' import LoadErrorState from '@/shared/components/LoadErrorState.vue' import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' @@ -11,7 +11,7 @@ import type { SourceReference } from './workApi' import { rejectedCommand, useWorkDraft } from './useWorkDraft' import { workReason } from './workPresentation' const props = defineProps<{ sources: SourceReference[]; canWrite: boolean }>() -const emit = defineEmits<{ saved: [] }>() +const emit = defineEmits<{ saved: [appointment: FbaAppointmentDto] }>() const access = useAccessPreviewStore(), preview = ref(null) const form = reactive(createAppointmentForm()), baseline = ref(JSON.stringify(form)) const loading = ref(false), saving = ref(false), error = ref(null), saveError = ref(null) @@ -35,7 +35,7 @@ async function save() { try { const result = await createSourceAppointment(pending.value) pending.value = null; baseline.value = JSON.stringify(form) - ElMessage.success(`预约安排 ${result.appointmentNo} 已保存`); emit('saved') + ElMessage.success(`预约安排 ${result.appointmentNo} 已保存`); emit('saved', result) } catch (cause) { saveError.value = getApiErrorDetails(cause, '创建结果待确认,请重试本次请求') if (rejectedCommand(cause)) { pending.value = null; await load() } diff --git a/src/modules/appointment-calendar/calendar.test.ts b/src/modules/appointment-calendar/calendar.test.ts index bfd0c581..3da14ba4 100644 --- a/src/modules/appointment-calendar/calendar.test.ts +++ b/src/modules/appointment-calendar/calendar.test.ts @@ -2,12 +2,17 @@ import { describe, expect, it } from 'vitest' import { addDays, calendarDates, calendarRange, moveCalendar } from './calendar' describe('appointment calendar', () => { - it('shows six complete Monday-first weeks across year boundaries', () => { - const dates = calendarDates('2027-01-15', 'month') - expect(dates).toHaveLength(42) - expect(dates[0]).toBe('2026-12-28') - expect(dates.at(-1)).toBe('2027-02-07') - expect(calendarRange('2027-01-15', 'month').to).toBe('2027-02-08') + it.each([ + ['2026-10-19', '2026-09-28', '2026-11-02', 35], + ['2027-01-15', '2026-12-28', '2027-02-01', 35], + ['2027-02-15', '2027-02-01', '2027-03-01', 28], + ['2026-08-15', '2026-07-27', '2026-09-07', 42], + ] as const)('shows only the complete weeks needed for %s', (anchor, from, to, count) => { + const dates = calendarDates(anchor, 'month') + expect(dates).toHaveLength(count) + expect(dates[0]).toBe(from) + expect(dates.at(-1)).toBe(addDays(to, -1)) + expect(calendarRange(anchor, 'month')).toEqual({ from, to }) }) it('moves by a week in week mode instead of following the prototype month-navigation bug', () => { diff --git a/src/modules/appointment-calendar/calendar.ts b/src/modules/appointment-calendar/calendar.ts index 0c4257fc..e6c2ba3d 100644 --- a/src/modules/appointment-calendar/calendar.ts +++ b/src/modules/appointment-calendar/calendar.ts @@ -29,16 +29,19 @@ export function addDays(value: string, days: number): string { export function calendarRange(anchor: string, mode: CalendarMode): { from: string; to: string } { const date = parseDate(anchor) + const nextMonth = new Date(date.getFullYear(), date.getMonth() + 1, 1, 12) if (mode === 'month') date.setDate(1) date.setDate(date.getDate() - (date.getDay() + 6) % 7) const from = dateKey(date) - // A half-open interval also works directly with database timestamps. - return { from, to: addDays(from, mode === 'month' ? 42 : 7) } + // 月视图只补齐月末所在周,不追加整周的下月日期;结束日期仍为开区间。 + return { from, to: mode === 'month' ? addDays(dateKey(nextMonth), (8 - nextMonth.getDay()) % 7) : addDays(from, 7) } } export function calendarDates(anchor: string, mode: CalendarMode): string[] { - const { from } = calendarRange(anchor, mode) - return Array.from({ length: mode === 'month' ? 42 : 7 }, (_, index) => addDays(from, index)) + const { from, to } = calendarRange(anchor, mode) + const dates: string[] = [] + for (let date = from; date < to; date = addDays(date, 1)) dates.push(date) + return dates } export function moveCalendar(anchor: string, mode: CalendarMode, offset: number): string { diff --git a/src/modules/appointment-calendar/planningApi.test.ts b/src/modules/appointment-calendar/planningApi.test.ts index 803bdf4c..d25f0444 100644 --- a/src/modules/appointment-calendar/planningApi.test.ts +++ b/src/modules/appointment-calendar/planningApi.test.ts @@ -1,6 +1,6 @@ import { expect, it, vi } from 'vitest' import { http } from '@/shared/api/http' -import { listPlanningCargo, listPlanningContainers, resolvePlanningOrder } from './planningApi' +import { listPlanningCargo, listPlanningDestinations, listPlanningContainers, resolvePlanningOrder } from './planningApi' vi.mock('@/shared/api/http', () => ({ http: { get: vi.fn() }, unwrapApiResponse: (value: { data: T }) => value.data })) it('uses the gateway planning route and preserves stable source IDs and null predictions', async () => { @@ -20,6 +20,15 @@ it('keeps basic container listing on its own endpoint rather than requiring impa expect(http.get).toHaveBeenCalledExactlyOnceWith('/platform/v1/last-leg/appointment-planning/containers', { params: { page: 1, size: 20 }, signal: undefined }) expect(result.total).toBe(0) }) +it('uses authoritative destination options and sends the selected destination to server pagination', async () => { + vi.mocked(http.get).mockClear() + vi.mocked(http.get).mockResolvedValueOnce({data:['ABE4','ONT8']}) + expect(await listPlanningDestinations()).toEqual(['ABE4','ONT8']) + expect(http.get).toHaveBeenCalledExactlyOnceWith('/platform/v1/last-leg/appointment-planning/cargo/destinations',{signal:undefined}) + vi.mocked(http.get).mockResolvedValueOnce({data:{records:[],page:2,size:20,total:0,pages:0}}) + await listPlanningCargo({page:2,size:20,keyword:'IN-A',destination:'ONT8'}) + expect(http.get).toHaveBeenLastCalledWith('/platform/v1/last-leg/appointment-planning/cargo',{params:{page:2,size:20,keyword:'IN-A',destination:'ONT8'},signal:undefined}) +}) it('resolves an order through the real canonical-source endpoint without fabricating an ORDER alias', async () => { vi.mocked(http.get).mockClear() vi.mocked(http.get).mockResolvedValue({data:{source:{sourceKey:'BOOKING:B-1'},coverage:{appointments:[]}}}) diff --git a/src/modules/appointment-calendar/planningApi.ts b/src/modules/appointment-calendar/planningApi.ts index 88f6f007..77762ff3 100644 --- a/src/modules/appointment-calendar/planningApi.ts +++ b/src/modules/appointment-calendar/planningApi.ts @@ -31,7 +31,7 @@ export interface PlanningCargo { mixedDestinations: boolean } export interface PlanningCargoPage { records: PlanningCargo[]; page: number; size: number; total: number; pages: number } -export async function listPlanningCargo(query: { page: number; size: number; keyword?: string }, signal?: AbortSignal) { +export async function listPlanningCargo(query: { page: number; size: number; keyword?: string; destination?: string }, signal?: AbortSignal) { const response = await http.get, ApiEnvelope>( '/platform/v1/last-leg/appointment-planning/cargo', { params: query, signal }, ) @@ -39,6 +39,12 @@ export async function listPlanningCargo(query: { page: number; size: number; key return { ...page, page: Number(page.page), size: Number(page.size), total: Number(page.total), pages: Number(page.pages) } } +export async function listPlanningDestinations(signal?: AbortSignal) { + return unwrapApiResponse(await http.get, ApiEnvelope>( + '/platform/v1/last-leg/appointment-planning/cargo/destinations', { signal }, + )) +} + export type FollowupStage = 'DRAYAGE_CONTACT' | 'TERMINAL_APPOINTMENT' | 'WAREHOUSE_RECEIVING' | 'FBA_RECEIVING' export interface FollowupRecord { stage: FollowupStage; version: number; contactedParty: string | null; result: string; nextAction: string | null; followupDate: string | null; operatorName: string | null; createdAt: string } export interface FollowupHistory { history: { records: FollowupRecord[]; page: number; size: number; total: number; pages: number }; versions: Record } diff --git a/src/styles/adminDensity.test.ts b/src/styles/adminDensity.test.ts index 7e197daa..ee48eb55 100644 --- a/src/styles/adminDensity.test.ts +++ b/src/styles/adminDensity.test.ts @@ -199,7 +199,8 @@ describe('approved Admin density ownership', () => { expect(declarations('.detail-events')['overflow-y']).toBe('auto') expect(declarations('.detail-event dd')['overflow-wrap']).toBe('anywhere') expect(panel.template!.content).toContain('.slice(0, 3)') - expect(panel.template!.content).not.toContain('新增预约') + expect(panel.template!.content).toContain('v-if="canCreate"') + expect(panel.template!.content).toContain('新增预约') }) it('shares list status and category tab styles across registered list consumers', () => { for (const file of [ From 5c28cd7c79748d281f541c9dfe865d1c2327832e Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 19 Oct 2026 17:56:31 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 93 +- docs/business-index.md | 65 ++ src/core/navigation/adminNavigation.test.ts | 12 +- src/core/navigation/adminNavigation.ts | 6 +- .../appointment-calendar/PROTOTYPE_MATRIX.md | 17 +- .../TripAppointmentPanel.test.ts | 2 +- .../appointment-calendar/WorkDetailPanel.vue | 78 +- .../WorkDispatchForm.test.ts | 262 +++-- .../appointment-calendar/WorkDispatchForm.vue | 270 +++-- .../WorkExistingDispatchForm.test.ts | 65 +- .../WorkExistingDispatchForm.vue | 68 +- .../WorkSharedTripForm.test.ts | 61 +- .../WorkSharedTripForm.vue | 65 +- .../pages/DomesticContainersView.vue | 10 +- src/modules/fee-items/PROTOTYPE_MATRIX.md | 12 + .../fee-items/api/platformFeeItems.test.ts | 1 + src/modules/fee-items/api/platformFeeItems.ts | 2 + .../fee-items/pages/FeeItemsView.test.ts | 22 +- src/modules/fee-items/pages/FeeItemsView.vue | 9 + .../measurement/InboundMeasurementPanel.vue | 4 +- .../loading-lists/api/containerFees.test.ts | 12 +- .../loading-lists/api/containerFees.ts | 3 +- .../components/ContainerFeesPanel.test.ts | 98 +- .../components/ContainerFeesPanel.vue | 25 +- src/modules/overseas-master-data/AGENTS.md | 2 +- .../overseas-master-data/PROTOTYPE_MATRIX.md | 15 + .../components/CapacityInquiryDialog.vue | 2 +- .../components/CapacityInquiryTable.vue | 2 +- .../components/TripOperationsCharts.test.ts | 101 ++ .../components/TripOperationsCharts.vue | 158 ++- src/modules/overseas-master-data/index.ts | 3 + .../pages/CapacityOfferDetailView.vue | 2 +- .../pages/CapacityOffersBehavior.test.ts | 25 + .../pages/CapacityOffersView.vue | 10 +- .../pages/FleetReportingViews.test.ts | 45 +- .../pages/TripOperationsView.vue | 85 +- src/modules/overseas-master-data/routes.ts | 4 +- src/modules/overseas-warehouse/AGENTS.md | 11 +- .../overseas-warehouse/api/dispatches.test.ts | 286 ++--- .../overseas-warehouse/api/dispatches.ts | 395 +------ .../components/DispatchArrangeFields.test.ts | 35 + .../components/DispatchArrangeFields.vue | 54 + .../components/DispatchEvidenceFiles.test.ts | 70 ++ .../components/DispatchEvidenceFiles.vue | 85 ++ .../components/DispatchResourceFields.test.ts | 28 + .../components/DispatchResourceFields.vue | 53 + .../components/DispatchWorkflowDialog.test.ts | 141 +++ .../components/DispatchWorkflowDialog.vue | 216 ++++ .../components/SmartDispatchDialog.test.ts | 421 ++----- .../components/SmartDispatchDialog.vue | 1030 +++-------------- .../dispatchCommandRecovery.test.ts | 63 + .../dispatchCommandRecovery.ts | 82 ++ .../overseas-warehouse/dispatchContracts.ts | 100 ++ .../dispatchPresentation.ts | 16 + .../dispatchTestFixtures.ts | 14 + .../overseas-warehouse/dispatchWire.test.ts | 89 ++ .../overseas-warehouse/dispatchWire.ts | 71 ++ src/modules/overseas-warehouse/index.ts | 8 +- .../layoutAcceptance.test.ts | 3 +- .../pages/DispatchDetailView.test.ts | 109 +- .../pages/DispatchDetailView.vue | 208 +--- .../pages/DispatchesView.test.ts | 445 ++----- .../pages/DispatchesView.vue | 808 ++----------- .../pages/InboundOrdersView.dispatch.test.ts | 53 + .../pages/InboundOrdersView.vue | 4 +- .../pages/LastMileOrdersView.behavior.test.ts | 28 +- .../pages/LastMileOrdersView.vue | 10 +- .../pages/prototypeContract.test.ts | 74 +- src/modules/overseas-warehouse/permissions.ts | 2 +- .../smartDispatchWarehouseTab.test.ts | 6 +- .../smartDispatchWarehouseTab.ts | 6 +- src/modules/overseas-warehouse/types.test.ts | 5 +- src/modules/overseas-warehouse/types.ts | 23 - .../pages/SailingCalendarView.test.ts | 14 + .../pages/SailingCalendarView.vue | 2 +- .../self-operated/pages/OverviewView.test.ts | 20 + .../self-operated/pages/OverviewView.vue | 8 +- .../api/inboundFeeDetails.test.ts | 6 +- .../unified-orders/api/inboundFeeDetails.ts | 2 + .../InboundFeeDetailManager.test.ts | 100 +- .../components/InboundFeeDetailManager.vue | 16 +- src/styles/adminDensity.css | 4 +- src/styles/adminDensity.test.ts | 40 + src/styles/adminTextActions.css | 6 + 84 files changed, 3356 insertions(+), 3630 deletions(-) create mode 100644 docs/business-index.md create mode 100644 src/modules/fee-items/PROTOTYPE_MATRIX.md create mode 100644 src/modules/overseas-master-data/components/TripOperationsCharts.test.ts create mode 100644 src/modules/overseas-warehouse/components/DispatchArrangeFields.test.ts create mode 100644 src/modules/overseas-warehouse/components/DispatchArrangeFields.vue create mode 100644 src/modules/overseas-warehouse/components/DispatchEvidenceFiles.test.ts create mode 100644 src/modules/overseas-warehouse/components/DispatchEvidenceFiles.vue create mode 100644 src/modules/overseas-warehouse/components/DispatchResourceFields.test.ts create mode 100644 src/modules/overseas-warehouse/components/DispatchResourceFields.vue create mode 100644 src/modules/overseas-warehouse/components/DispatchWorkflowDialog.test.ts create mode 100644 src/modules/overseas-warehouse/components/DispatchWorkflowDialog.vue create mode 100644 src/modules/overseas-warehouse/dispatchCommandRecovery.test.ts create mode 100644 src/modules/overseas-warehouse/dispatchCommandRecovery.ts create mode 100644 src/modules/overseas-warehouse/dispatchContracts.ts create mode 100644 src/modules/overseas-warehouse/dispatchPresentation.ts create mode 100644 src/modules/overseas-warehouse/dispatchTestFixtures.ts create mode 100644 src/modules/overseas-warehouse/dispatchWire.test.ts create mode 100644 src/modules/overseas-warehouse/dispatchWire.ts create mode 100644 src/modules/overseas-warehouse/pages/InboundOrdersView.dispatch.test.ts diff --git a/AGENTS.md b/AGENTS.md index 1d2ee46d..c457b9ea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,61 +36,52 @@ ## 模块快速路由 -表中 `modules/`、`core/`、`shared/`、`app/` 和 `main.ts` 均相对 `src/`;裸文件名沿用同一单元格最近目录。模块细节只在最近指引维护,应用层保留入口。 +按短表定位后按需查 [业务细则](docs/business-index.md);涉及业务行为变更时必须核对对应细则及最近模块指引。从应用根执行 `rg -n -F '任务词或文件名' docs/business-index.md`,只读命中条款。 -| 业务 | Admin 入口 | 联查 | -|---|---|---| -| 登录、会话与统一 Token | `modules/auth/`、`core/auth/` | 国内 `auth/`、`iam/`;海外请求客户端再读 `shared/api/AGENTS.md` | -| 启动与懒加载资源恢复 | `main.ts`、`core/routing/navigationRecovery.ts` | 仅识别动态模块/Chunk 资源过期;30 秒内同目标最多自动恢复一次,存在未保存编辑时必须先确认 | -| 工作台与指标 | `modules/dashboard/` | 指标所属模块与服务端,不复制领域契约 | -| 自营销量、库存与数据采集 | `modules/self-operated/AGENTS.md` | 三个入口 `/self-operated/sales`、`/self-operated/inventory`、`/self-operated/collection`;后端 `lastleg-service/selfoperated/analytics/`,正式合同见 `../../docs/自营业务/README.md`。 | -| 船期日历 | `modules/sailing-calendar/` | 当前日历入口覆盖渠道、周模板、单次船期、收货截止时间和按渠道货量统计;后端按 `firstleg/` 船期接口定位,不读取已归档的货柜开发规划 | -| 预约管理、预约列表、跨客户同仓预约 | `modules/appointment-calendar/`;入仓单预约沿 `modules/overseas-warehouse/` | 统一规则见 `../../docs/海外仓业务/README.md` 的预约送仓;同仓跨客户共用预约,客户归属在货物明细;到港只读 Firstleg,送仓归 Lastleg | -| FBA 无匹配价格、人工定价 | `modules/inbounds/fee/api.ts` 的 `createManualInboundQuote`、`types.ts`;页面消费沿 `InboundFeePanel.vue` 核对 | `POST /manual-quote` 提交计费重量、体积、单价、原因和 `Idempotency-Key`;Backend `PlatformFbaPricingController.manualQuote` → `FbaPricingService.manualQuote`,权限 `firstleg:inbound:confirm-send-fee`。仅 FBA、已有实际测量且当前价格未命中时允许,客户已确认费用禁止改价;`MANUAL_QUOTE` 快照允许价格记录与版本为空,不伪造价格簿关联。API 回归 `modules/inbounds/fee/api.test.ts`,不能以 API 存在代替页面接入完成 | -| PDA版本、APK下载、WGT热更新发布与停发 | `modules/pda-releases/pages/PdaReleasesView.vue`、`modules/pda-releases/api/pdaReleases.ts`、`modules/pda-releases/routes.ts` | 系统菜单 `/system/pda-releases`;列表权限 `platform:pda-release:list`,上传/发布/停发权限 `platform:pda-release:manage`。国内/海外按 `domestic/overseas` 分开查询,上传只创建草稿,发布与停发需确认;管理请求使用国内 HTTP 的 `/platform/v1/files/pda-releases`,后端见 `file/AGENTS.md`,设备安装与重启查对应 PDA 的更新工具 | -| 客户、企业审核、客户统计 | `modules/customers/` | `iam/`、`auth/`、`file/`、`firstleg/customerstatistics/`;公司联系电话只作企业联系信息,不修改管理员/成员登录手机号且不要求跨企业唯一。审核必填资质仅营业执照与统一社会信用代码;法人身份证选填,可随资质包提交,也可在已有营业执照后单独补传或替换 | -| 供应商、国内整柜 | `modules/suppliers/`、`modules/domestic-containers/` | Backend `warehouse/` 及对应 OpenAPI | -| 平台仓库与仓储资源 | 仓库主档 `modules/warehouses/`,国内资源 `modules/warehouse-resources/` | 全部仓库 ID、类型、状态、地址和能力归国内 `warehouse/`;海外 `warehouse-overseas-service/` 只维护引用同一 warehouseId 的库位、托盘和锁 | -| 平台用户、RBAC、导航 | `modules/iam/`、`core/access/`、`core/navigation/` | `auth/`、`iam/`、平台权限契约 | -| 国内/海外 PDA 独立人员与海外仓绑定 | `modules/iam/pages/InternalUsersView.vue`、`modules/iam/api/platformIam.ts` | 国内 `CN_FIRSTLEG_PDA_OPERATOR` 与海外 `OVERSEAS_WAREHOUSE_PDA_OPERATOR` 互斥;只有海外角色必须且只能绑定一个 `overseasWarehouseId`,候选来自国内 Warehouse 平台仓库 API 的公共 `OVERSEAS + ACTIVE + RECEIVING` 收货仓 | -| 消息中心 | `modules/notifications/` | Backend `iam/.../notification/`;仅顶部消息中心,不进入左侧菜单 | -| 订单列表、批量审核预览、详情、商品图片与入库费用 | `modules/unified-orders/`、`modules/first-leg-orders/`;平台商品图片由 `modules/unified-orders/api/unifiedOrders.ts` 按客户与 fileId 下载 | `order/`、`firstleg/`、Finance;旧系统导入固定归属“IT测试公司”,前端不提交或选择客户,逐行结果展示入仓码与失败原因。图片读取归 `file/` 的 `PlatformProductImageController`。Order 状态为 `SUBMITTED` 时统一列表必须保持 `PENDING_REVIEW/APPROVE_ORDER`,不得被已存在的 Firstleg 准备中入库投影覆盖。批量审核弹框通过 `unifiedOrders.ts` 的 `/orders/first-leg/batch-review-preview` 一次取回全部预览,按服务端 `canApprove/blockReason` 展示,不逐票请求详情或用内部 ID 作标题 | -| 客户原始申报资料编辑与渠道变更 | `modules/unified-orders/components/OrderDeclarationEditorDialog.vue`、`api/unifiedOrders.ts`、`pages/UnifiedOrdersView.vue` | 使用订单版本和货物 `itemNo` 整体提交,不允许增删货物行;只编辑客户申报快照,不覆盖仓库/PDA 实测、测量审核或计费事实。渠道变化需二次确认并由 Backend `order/` 联查 `firstleg/` 的分柜、同柜、装柜与重新计价门禁;国内收货仓或中转仓变化必须重新下单 | -| 入库扣货、解除与凭证历史 | `modules/unified-orders/pages/UnifiedOrdersView.vue`、`modules/inbounds/core/api.ts`、`types.ts` | 页面只消费服务端 `holdFlag/currentHoldReason/canHold/canRelease/actionBlockReason/history`;扣货与解除提交当前入库版本、幂等键、必填原因和可选凭证,凭证下载只走 Firstleg 授权入口。Backend `firstleg/` 的 `InboundHoldService`、`InboundHoldGuard`、`InboundHoldEvidenceService`,文件归 `file/` | -| 入库、测量、称重库位状态、客户箱规策略与计价主数据 | 测量弹框/工作区 `modules/inbounds/measurement/InboundMeasurementDialog.vue`、`InboundMeasurementPanel.vue`,称重 `modules/inbounds/weight/components/WeightOcrPanel.vue`;主数据 `modules/fba-pricing/`、`modules/dimension-config/`、`modules/customers/components/CustomerBillingRulesPanel.vue`、`modules/fee-items/` | 客户固定四档箱规,默认计算方式为 `ADD/MULTIPLY`;测量行可选实际尺寸、箱规尺寸策略或客户默认尺寸系数,保存后以 Backend `firstleg/` 返回的外箱尺寸、体积重和计费基数为准,不在页面独立重算。费用确认的实际箱数、毛重和体积优先汇总当前 `actualMeasurementRows`,不得用可能滞后的工作区顶层合计覆盖明细。称重页在每张图片卡片只展示服务端 `locationCode/slotStatus/sharedOverflow`,不渲染或自行推导整票预留引导;删除图片须提示槽位仍归本票保留,不得按图片数组下标推导托盘或库位 | -| 一件代发托盘打印 | `modules/unified-orders/pages/UnifiedOrdersView.vue` 的“托盘打印”页签、`modules/inbounds/core/DropshipPalletPrintPanel.vue`、`core/api.ts` | 仅一件代发订单展示;测量审核预生成托盘码后由 Admin 电脑端打印,支持本票全部托盘合并打印和单托补打。PDA 只扫码组托,不承担 PDF 下载或打印机连接 | -| 入库单扣货、解除、凭证与历史 | `modules/inbounds/pages/InboundOrdersView.vue`、详情工作区、`modules/inbounds/core/api.ts` | 列表展示服务端 `holdFlag/currentHoldReason` 并支持筛选;详情只按 `canHold/canRelease/actionBlockReason` 开放操作。扣货与解除提交当前 `version`、原因和独立幂等键,可选凭证先按明确客户与入库单上传;历史文件只调用专用下载入口,不把上传成功但未提交命令的文件冒充扣货证据 | -| 客户专属价格簿、柜级阶梯运费与受控调价 | 列表/上传 `modules/fba-pricing/carrier-rates/pages/CarrierRatesView.vue`,版本详情/仓库映射/确认 `modules/fba-pricing/carrier-rates/pages/CarrierRateDetailView.vue`,路由 `modules/fba-pricing/routes.ts`;`modules/inbounds/fee/`、`modules/unified-orders/components/InboundFeeDetailManager.vue` | 列表与上传后按 `versionId` 进入 `fba-carrier-rate-detail` 独立详情页,价格行只读;回归入口 `modules/fba-pricing/carrier-rates/pages/CarrierRatesView.test.ts`。价格簿页面显式选择公共报价或具体客户;列表、上传、确认只作用于该 `customerId`,不得混合版本。计价优先消费客户专属 ACTIVE 版本,无专属版本才回退公共 ACTIVE;柜级阶梯金额和各客户分摊只展示服务端结果。调整计费量只能在服务端返回的上下 10% 范围内并提交当前费用版本、原因与二次确认;私人仓尾程派送费使用稳定 `PRIVATE_LAST_MILE_DELIVERY` 代码且只对 `PRIVATE` 目的地开放;一件代发须在预装确认前完成试算,计价后保持服务端 `WAIT_PALLETIZE` 状态 | -| 财务、客户资金账户与柜级公共费用分摊 | `modules/finance/`,继续读该目录 `AGENTS.md`;客户账户 `pages/FinanceCustomerAccountsView.vue`,柜费 `modules/loading-lists/components/ContainerFeesPanel.vue`、`api/containerFees.ts` | `finance/`、`firstleg/`、`file/`、`docs/财务管理/`;实际、冻结、可用余额、授信与流水只消费 Finance 权威结果,充值审核和手工充扣携带版本与幂等键。跨客户公共柜费只按服务端柜内客户/订单事实分摊,金额必须守恒,创建与审核、驳回、调整、作废均整组原子处理 | -| 货柜双工作台、排柜、预装/装柜清单、真实柜号后补与整柜报关 | 国内 `modules/container-loading/pages/DomesticContainersView.vue`、海外 `OverseasContainersView.vue`,共享详情实现 `ContainerDetailView.vue`;预装详情 `modules/loading-lists/pages/PreloadPlanDetailView.vue`,资料/PDA 证据 `components/ContainerOverviewPanel.vue`,报关 `components/ContainerCustomsPanel.vue` | 国内“送柜报关”只消费 `/containers/domestic` 与服务端 `allowedActions`,承载模拟柜、订舱关联、智能装柜;模拟柜创建选择与智能装柜一致的 9 类计划渠道,未关联订舱前实际渠道为空,订舱关联候选按计划渠道组匹配并同时展示订舱号与提单号、装柜清单页签、出口报关和国内出仓;创建柜次时自动生成唯一预装计划,预装详情先调用 `/loading-plans/{planId}/lifecycle`,完成后深链进入国内货柜详情的装柜清单页签,独立装柜清单导航 `/loading-lists` 复用已确认清单 API,查看进入独立详情路由 `/loading-lists/:planId`,保留返回列表和打印;独立清单详情与货柜清单页签共同使用 `useLoadingListDetail.ts` 加载、校验和处理错误,`LoadingListDetailDrawer.vue` 统一密度、摘要、明细、历史及打印入口,打印实现归 `printLoadingList.ts`,外层只保留返回目标与货柜专属 PDA/报关区域;货柜列表经 `ContainerWorkbenchView.vue` 在国内装柜与海外清关提柜之间按权限切换。订舱关联允许真实集装箱号为空,但确认预装和创建 PDA 任务前必须补齐;历史已建但缺号的任务仍可在预装详情首次补录,保存后恢复 PDA 可进入状态;全买单柜继续使用 `complete-all-buy`。海外“清关提柜”只消费 `/containers/overseas`,承载船节点、目的港清关、DO/PU、码头车队和还空;原预约/实际提柜展示位改为整柜预计/实际到仓,预计到仓在列表内人工快捷维护并经海外接口同步整柜尾程订单,实际到仓只读海外 PDA 首次开始收货时间。还空截止事实由服务端基于卸柜完成时间加 72 小时计算,前端不重算。两端统一使用 `containerBookingId`,列表范围由服务端 `scope=ACTIVE/HISTORY` 分页,禁止浏览器筛选或本地推导写资格。后端为 `LoadingListService`、`firstleg/customs/container/`、`firstleg/terminaldrayage/` 和 `lastleg-service/`,文件归 `file/` | -| 合柜报关 / 同柜不可拆组 | `modules/unified-orders/api/coLoadGroups.ts`、`pages/UnifiedOrdersView.vue`;装柜消费 `modules/container-loading/pages/SmartLoadingWorkbenchView.vue` | 前端只提交订单 ID;Backend `firstleg/loading/coload/` 展开并冻结每票全部可用入仓片段。成员数是入仓片段数,不等于订单票数;只要求同国内仓、同渠道,允许跨客户及不同目的仓港;分配、移除、取消均整组处理 | -| 全局智能装柜工作台、入仓码候选与箱数分配 | `modules/container-loading/components/SmartLoadingDialog.vue` 内嵌 `pages/GlobalSmartLoadingView.vue`,柜次页 `SmartLoadingWorkbenchView.vue`,以及 `smartLoadingChannelGroups.ts`、`smartLoadingItems.ts`、`api/platformContainerLoading.ts`;全局页当前不注册独立路由 | Backend `firstleg/` 的 `/inbound-candidates`、`/containers`、`/inbound-assignments`、`/plans/{planId}/pending-review-assignments`、`/group-assignments`、`/unpicked-items`。`orderStatuses` 最多四项且未传表示全部未完成状态;候选状态多选覆盖待审核、待测量、待分柜、装柜中,状态只用于筛选展示,按钮资格以服务端 `canAssign` 为准;待审核/待测量可提前占位但不得提前确认预装或启动 PDA。计划箱数编辑上限、已用和未用展示只消费服务端 `totalPackageCount/remainingPackageCount`,不得从候选页或本地规格重新拼算。人工加入允许跨渠道,智能推荐仍按柜次渠道。柜次容量和限重取服务端字段;同柜组作为单卡整组分页、分配和移除,任一成员已拣时整组保留 | -| 国内货柜履约订舱资料、打印、关联候选与出仓前船期 | `modules/ocean-bookings/` 的 `components/OceanBookingDocumentsPanel.vue`、`OceanBookingPrintDialog.vue`,以及 `modules/container-loading/`、`modules/port-schedules/` | 订舱草稿按当前版本维护资料,放舱后资料与表单只读;打印对草稿与已放舱订舱均开放,按当前已保存资料生成,未填写字段显示“—”。`platformOceanBookings.ts` 的 `/link-candidates` 只消费服务端分页,`containerCount` 是柜型需求 `quantity` 总和;Backend `firstleg/oceanbooking/`、`file/`、`print/`、`firstleg/portschedule/` | -| 订舱级拖车预约 | `modules/trucking/api/platformTrucking.ts` 的 `/candidates`、`components/TruckAppointmentForm.vue`、`pages/TruckAppointmentCreateView.vue`、`TruckAppointmentsView.vue` | 表单只选择服务端返回的 `RELEASED` 订舱与 `TRUCKING + truckingScope=DOMESTIC` 国内拖车供应商,不选择或提交客户、柜次、装柜计划或真实集装箱号;清关提柜固定选择 `OVERSEAS` 海外车队,类型在供应商主档单选维护;提单号和仓库由订舱冻结事实带入。一个订舱最多一张非终态拖车单,重复创建转入现有单继续编辑;PDA 完成不自动推进拖车。附件为平台作用域且不重复提单文件;Backend `TruckAppointmentService`、`truck-appointments-v1.yaml` | -| 目的港码头拖车与还空预警 | 海外清关提柜工作台/详情 `modules/container-loading/pages/OverseasContainersView.vue`、`OverseasContainerDetailView.vue`,API `modules/container-loading/api/terminalDrayages.ts`、`containerArrivalTimes.ts` | 与订舱级拖车预约是不同生命周期;按 `containerBookingId` 读取服务端状态、`allowedActions` 和历史,不能复用订舱级拖车表单或自行推进状态。列表“预计到仓”列直接提供人工日期时间选择器,保存时通过 Lastleg 柜级版本和幂等键原子更新该柜全部尾程订单,已有入库计划后禁止修改;“实际到仓”只读 `ll_receiving_container_session.receiving_at`,即海外 PDA 首次开始收货时间。还空截止、超时天/小时只展示服务端基于权威卸柜完成时间加 72 小时计算的结果;Backend `firstleg/terminaldrayage/`、`lastleg-service/`,邮件记录通过 `terminalDrayages.ts` 查询与重试。 | -| 历史报关协同与国内出仓 | `modules/customs/`、`modules/outbound-dispatch/` | `modules/customs/` 是历史报关案件/清单/代理提交与回执入口;装柜完成后的整柜退税/买单资料从货柜详情进入,不在旧模块复制;Backend `firstleg/outbound/dispatch/`、`firstleg/customs/`、`file/` | -| 主数据、下载、退避任务重试、审计 | `modules/master-data/`、`modules/downloads/pages/DownloadCenterView.vue`、`modules/audit/` | File 导出任务返回 `attemptCount/maxAttempts/nextRetryAt`;仅失败或已进入自动退避的等待任务显示重试,等待任务重试用于立即执行当前投递,不创建新任务;联查 `warehouse/`、`file/`、`audit-service/` | -| 海外接柜、入库、库存与库内服务 | `modules/overseas-warehouse/` | `gateway/`、`lastleg-service/`、`warehouse-overseas-service/`;与排车、出库、派送、异常退回和一件代发共同组成海外仓全流程 | -| 海外采购、卡车、散板 | `modules/overseas-business/` | `gateway/`、`lastleg-service/` | -| 海外仓业务合作仓与通用资料 | 合作仓 `modules/warehouses/`;报价/汇率/车辆 `modules/overseas-master-data/` | 国内 `warehouse/`、海外实际资源与服务 | -| 海外仓业务国内出仓后船节点 | `modules/port-schedules/`、`modules/ocean-bookings/`、货柜详情 | `firstleg/portschedule/`、`integration-service/`;预计节点只展示不推进实际进度,手工实际节点必须严格向后追加并防重复,十节点标准见 `../../docs/海外仓业务/船节点标准化契约.md` | -| 海外排车、出库与派送 | `modules/overseas-warehouse/AGENTS.md` | 按模块表定位排车、出库、派送页面及 API;履约边界在模块内维护 | -| 海外异常退回与客户回访 | `modules/overseas-warehouse/AGENTS.md` | 派送异常退回区别于退货库存;页面、API 与原子推进约束见模块指引 | +路径从应用根起算,`modules/`、`core/` 从 `src/` 起算;接口片段与符号沿同一行代码定位。 -全量验收按业务从 `../../docs/订单与客户/`、`入库测量与计费/`、`国内货柜履约/`、`财务管理/` 选择最小契约;海外跨模块流程或入口不明时读 `../../docs/海外仓业务/README.md`,已定位局部任务直接沿模块入口。进度、排期或验收状态问题再读取对应进度表和报告。 - -散货托盘码打印:`modules/loading-lists/components/PalletLabelPrintDialog.vue`、`modules/loading-lists/api/platformLoadingLists.ts`,由货柜详情与装柜清单详情进入;先调用 `/pallet-labels/prepare` 获取服务端按 FBA 仓分组的冻结清单,再通过 `/pallet-labels/print` 下载 PDF。托盘码使用“真实柜号-FBA 地址内部编号-三位序号”,页面同时展示 FBA 仓码和内部编号;支持全部打印或按仓指定张数补打,一件代发托盘不在该清单,前端不生成或重新编号托盘码。 +| 业务 | 首个入口 | +|---|---| +| 登录、会话与统一 Token | `modules/auth/`、`core/auth/` | +| 启动与懒加载资源恢复 | `src/main.ts`、`core/routing/navigationRecovery.ts` | +| 工作台与指标 | `modules/dashboard/` | +| 自营销量、库存与数据采集 | `modules/self-operated/AGENTS.md` | +| 船期日历 | `modules/sailing-calendar/` | +| 预约管理、预约列表、跨客户同仓预约 | `modules/appointment-calendar/`、`modules/overseas-warehouse/` | +| FBA 无匹配价格、人工定价 | `modules/inbounds/fee/api.ts`、`createManualInboundQuote` | +| PDA版本、APK下载、WGT热更新发布与停发 | `modules/pda-releases/pages/PdaReleasesView.vue`、`modules/pda-releases/api/pdaReleases.ts` | +| 客户、企业审核、客户统计 | `modules/customers/` | +| 供应商、国内整柜 | `modules/suppliers/`、`modules/domestic-containers/` | +| 平台仓库与仓储资源 | `modules/warehouses/`、`modules/warehouse-resources/` | +| 平台用户、RBAC、导航 | `modules/iam/`、`core/access/` | +| 国内/海外 PDA 独立人员与海外仓绑定 | `modules/iam/pages/InternalUsersView.vue`、`modules/iam/api/platformIam.ts` | +| 消息中心 | `modules/notifications/` | +| 订单列表、批量审核预览、详情、商品图片与入库费用 | `modules/unified-orders/`、`modules/first-leg-orders/` | +| 客户原始申报资料编辑与渠道变更 | `modules/unified-orders/components/OrderDeclarationEditorDialog.vue`、`modules/unified-orders/api/unifiedOrders.ts` | +| 入库扣货、解除与凭证历史 | `modules/unified-orders/pages/UnifiedOrdersView.vue`、`modules/inbounds/core/api.ts` | +| 入库、测量、称重库位状态、客户箱规策略与计价主数据 | `modules/inbounds/measurement/InboundMeasurementDialog.vue`、`modules/inbounds/measurement/InboundMeasurementPanel.vue` | +| 一件代发托盘打印 | `modules/unified-orders/pages/UnifiedOrdersView.vue`、`modules/inbounds/core/DropshipPalletPrintPanel.vue` | +| 入库单扣货、解除、凭证与历史 | `modules/inbounds/pages/InboundOrdersView.vue`、`modules/inbounds/core/api.ts` | +| 客户专属价格簿、柜级阶梯运费与受控调价 | `modules/fba-pricing/carrier-rates/pages/CarrierRatesView.vue`、`modules/fba-pricing/carrier-rates/pages/CarrierRateDetailView.vue` | +| 财务、客户资金账户与柜级公共费用分摊 | `modules/finance/AGENTS.md` | +| 货柜双工作台、排柜、预装/装柜清单、真实柜号后补与整柜报关 | `modules/container-loading/pages/DomesticContainersView.vue`、`modules/container-loading/pages/OverseasContainersView.vue` | +| 合柜报关 / 同柜不可拆组 | `modules/unified-orders/api/coLoadGroups.ts`、`modules/unified-orders/pages/UnifiedOrdersView.vue` | +| 全局智能装柜工作台、入仓码候选与箱数分配 | `modules/container-loading/components/SmartLoadingDialog.vue`、`modules/container-loading/pages/GlobalSmartLoadingView.vue` | +| 国内货柜履约订舱资料、打印、关联候选与出仓前船期 | `modules/ocean-bookings/`、`modules/ocean-bookings/components/OceanBookingDocumentsPanel.vue` | +| 订舱级拖车预约 | `modules/trucking/api/platformTrucking.ts`、`/candidates` | +| 目的港码头拖车与还空预警 | `modules/container-loading/pages/OverseasContainersView.vue`、`modules/container-loading/pages/OverseasContainerDetailView.vue` | +| 历史报关协同与国内出仓 | `modules/customs/`、`modules/outbound-dispatch/` | +| 主数据、下载、退避任务重试、审计 | `modules/master-data/`、`modules/downloads/pages/DownloadCenterView.vue` | +| 海外接柜、入库、库存与库内服务 | `modules/overseas-warehouse/` | +| 海外采购、卡车、散板 | `modules/overseas-business/` | +| 海外仓业务合作仓与通用资料 | `modules/warehouses/`、`modules/overseas-master-data/` | +| 海外仓业务国内出仓后船节点 | `modules/port-schedules/`、`modules/ocean-bookings/` | +| 海外排车、出库与派送 | `modules/overseas-warehouse/AGENTS.md` | +| 海外异常退回与客户回访 | `modules/overseas-warehouse/AGENTS.md` | ## 海外模块内部快速索引(海外仓全流程) -- 海外仓的尾程订单控制与柜级入仓计划转换、退货库存、排车出库、异常回访:读取 `src/modules/overseas-warehouse/AGENTS.md`,字段与流程仅在该模块维护。 - -| 关键词 | 页面/路由入口 | API 与联查 | -|---|---|---| -| 海外采购 | `modules/overseas-business/pages/PurchaseOrdersView.vue`、详情/编辑页 | `modules/overseas-business/api/platformOverseasBusiness.ts` 的 purchase 方法;Backend `lastleg-service/procurement/` | -| 美国卡车、本土散板 | `modules/overseas-business/pages/TransportOrdersView.vue`、`TransportOrderFormView.vue` | 同一 API 的 us-truck/looseboard 方法;Backend `lastleg-service/trucking/`、`looseboard/` | -| 海外报价、月度汇率、车辆 | `modules/overseas-master-data/pages/OverseasQuotesView.vue`、`ExchangeRatesView.vue`、`VehiclesView.vue` | `modules/overseas-master-data/api/overseasMasterData.ts`;Backend `lastleg-service/masterdata/` | -| 司机档案、驾驶证、到期提醒与资格预览 | `modules/overseas-master-data/pages/DriversView.vue`、`DriverDetailView.vue`;最近指引 `modules/overseas-master-data/AGENTS.md` | `api/drivers.ts`;Backend `lastleg-service/fleet/driver/`、File `FLEET_DRIVER_LICENSE` | +海外仓作业先读 `src/modules/overseas-warehouse/AGENTS.md`;采购、车队等页面/API 联查按需查 `docs/business-index.md` 同名节。 ## 业务、安全与共享装配 @@ -127,7 +118,7 @@ pnpm typecheck pnpm build ``` -- 普通变更至少运行 `pnpm typecheck` 和受影响测试;交付前运行 `pnpm test` 与 `pnpm build`。当前没有 lint 脚本,不声称 lint 通过。 +- 源码、依赖或构建配置变更至少运行 `pnpm typecheck` 和受影响测试,交付前运行 `pnpm test` 与 `pnpm build`;不影响代码、契约或配置的纯文档/索引变更只检查路径、链接及索引合同。当前没有 lint 脚本,不声称 lint 通过。 - 不手工修改 `dist/`、`node_modules/` 或生成声明;依赖变化同步 `package.json` 与锁文件。 - 测试环境全量验收入口为 `../../baiyue-firstleg/scripts/admin-test-env-acceptance.mjs`,模块说明见同仓库 `scripts/admin-test-env-modules/README.md`;它会写入或停用测试数据,只有本轮明确授权才能运行。 - `pnpm test/typecheck` 和本地回归脚本不代表真实服务验收完成;HTTP 验收后仍由人工完成页面和截图确认。 diff --git a/docs/business-index.md b/docs/business-index.md new file mode 100644 index 00000000..b5e72cbb --- /dev/null +++ b/docs/business-index.md @@ -0,0 +1,65 @@ +# Admin业务详细索引 + +本文件保存当前页面/API 入口及业务规则,是[应用指引](../AGENTS.md)的按需补充。涉及字段、状态、权限、金额、资源或作业恢复时必须读取受影响条款,不作为普通定位任务的全文首读。 + +正文代码路径以工作区 `apps/admin-web/` 应用根起算,源码包缩写与裸文件名沿对应条目的目录解释;明确标注“工作区”的路径从工作区根起算。Markdown 相对链接以本文件所在的 `docs/` 为起点。已知文件直接定位;行为变更同时核对最近模块指引及服务端合同。 + +原条款在本文件维护,主 AGENTS 仅保留短入口和通用边界;不要将新业务细节复制回主指引。 + +## 模块快速路由 + +表中 `modules/`、`core/`、`shared/`、`app/` 和 `main.ts` 均相对 `src/`;裸文件名沿用同一单元格最近目录。模块细节只在最近指引维护,应用层保留入口。 + +| 业务 | Admin 入口 | 联查 | +|---|---|---| +| 登录、会话与统一 Token | `modules/auth/`、`core/auth/` | 国内 `auth/`、`iam/`;海外请求客户端再读 `shared/api/AGENTS.md` | +| 启动与懒加载资源恢复 | `main.ts`、`core/routing/navigationRecovery.ts` | 仅识别动态模块/Chunk 资源过期;30 秒内同目标最多自动恢复一次,存在未保存编辑时必须先确认 | +| 工作台与指标 | `modules/dashboard/` | 指标所属模块与服务端,不复制领域契约 | +| 自营销量、库存与数据采集 | `modules/self-operated/AGENTS.md` | 三个入口 `/self-operated/sales`、`/self-operated/inventory`、`/self-operated/collection`;后端 `lastleg-service/selfoperated/analytics/`,正式合同见 `../../docs/自营业务/README.md`。 | +| 船期日历 | `modules/sailing-calendar/` | 当前日历入口覆盖渠道、周模板、单次船期、收货截止时间和按渠道货量统计;后端按 `firstleg/` 船期接口定位,不读取已归档的货柜开发规划 | +| 预约管理、预约列表、跨客户同仓预约 | `modules/appointment-calendar/`;入仓单预约沿 `modules/overseas-warehouse/` | 统一规则见 `../../docs/海外仓业务/README.md` 的预约送仓;同仓跨客户共用预约,客户归属在货物明细;到港只读 Firstleg,送仓归 Lastleg | +| FBA 无匹配价格、人工定价 | `modules/inbounds/fee/api.ts` 的 `createManualInboundQuote`、`types.ts`;页面消费沿 `InboundFeePanel.vue` 核对 | `POST /manual-quote` 提交计费重量、体积、单价、原因和 `Idempotency-Key`;Backend `PlatformFbaPricingController.manualQuote` → `FbaPricingService.manualQuote`,权限 `firstleg:inbound:confirm-send-fee`。仅 FBA、已有实际测量且当前价格未命中时允许,客户已确认费用禁止改价;`MANUAL_QUOTE` 快照允许价格记录与版本为空,不伪造价格簿关联。API 回归 `modules/inbounds/fee/api.test.ts`,不能以 API 存在代替页面接入完成 | +| PDA版本、APK下载、WGT热更新发布与停发 | `modules/pda-releases/pages/PdaReleasesView.vue`、`modules/pda-releases/api/pdaReleases.ts`、`modules/pda-releases/routes.ts` | 系统菜单 `/system/pda-releases`;列表权限 `platform:pda-release:list`,上传/发布/停发权限 `platform:pda-release:manage`。国内/海外按 `domestic/overseas` 分开查询,上传只创建草稿,发布与停发需确认;管理请求使用国内 HTTP 的 `/platform/v1/files/pda-releases`,后端见 `file/AGENTS.md`,设备安装与重启查对应 PDA 的更新工具 | +| 客户、企业审核、客户统计 | `modules/customers/` | `iam/`、`auth/`、`file/`、`firstleg/customerstatistics/`;公司联系电话只作企业联系信息,不修改管理员/成员登录手机号且不要求跨企业唯一。审核必填资质仅营业执照与统一社会信用代码;法人身份证选填,可随资质包提交,也可在已有营业执照后单独补传或替换 | +| 供应商、国内整柜 | `modules/suppliers/`、`modules/domestic-containers/` | Backend `warehouse/` 及对应 OpenAPI | +| 平台仓库与仓储资源 | 仓库主档 `modules/warehouses/`,国内资源 `modules/warehouse-resources/` | 全部仓库 ID、类型、状态、地址和能力归国内 `warehouse/`;海外 `warehouse-overseas-service/` 只维护引用同一 warehouseId 的库位、托盘和锁 | +| 平台用户、RBAC、导航 | `modules/iam/`、`core/access/`、`core/navigation/` | `auth/`、`iam/`、平台权限契约 | +| 国内/海外 PDA 独立人员与海外仓绑定 | `modules/iam/pages/InternalUsersView.vue`、`modules/iam/api/platformIam.ts` | 国内 `CN_FIRSTLEG_PDA_OPERATOR` 与海外 `OVERSEAS_WAREHOUSE_PDA_OPERATOR` 互斥;只有海外角色必须且只能绑定一个 `overseasWarehouseId`,候选来自国内 Warehouse 平台仓库 API 的公共 `OVERSEAS + ACTIVE + RECEIVING` 收货仓 | +| 消息中心 | `modules/notifications/` | Backend `iam/.../notification/`;仅顶部消息中心,不进入左侧菜单 | +| 订单列表、批量审核预览、详情、商品图片与入库费用 | `modules/unified-orders/`、`modules/first-leg-orders/`;平台商品图片由 `modules/unified-orders/api/unifiedOrders.ts` 按客户与 fileId 下载 | `order/`、`firstleg/`、Finance;旧系统导入固定归属“IT测试公司”,前端不提交或选择客户,逐行结果展示入仓码与失败原因。图片读取归 `file/` 的 `PlatformProductImageController`。Order 状态为 `SUBMITTED` 时统一列表必须保持 `PENDING_REVIEW/APPROVE_ORDER`,不得被已存在的 Firstleg 准备中入库投影覆盖。批量审核弹框通过 `unifiedOrders.ts` 的 `/orders/first-leg/batch-review-preview` 一次取回全部预览,按服务端 `canApprove/blockReason` 展示,不逐票请求详情或用内部 ID 作标题 | +| 客户原始申报资料编辑与渠道变更 | `modules/unified-orders/components/OrderDeclarationEditorDialog.vue`、`api/unifiedOrders.ts`、`pages/UnifiedOrdersView.vue` | 使用订单版本和货物 `itemNo` 整体提交,不允许增删货物行;只编辑客户申报快照,不覆盖仓库/PDA 实测、测量审核或计费事实。渠道变化需二次确认并由 Backend `order/` 联查 `firstleg/` 的分柜、同柜、装柜与重新计价门禁;国内收货仓或中转仓变化必须重新下单 | +| 入库扣货、解除与凭证历史 | `modules/unified-orders/pages/UnifiedOrdersView.vue`、`modules/inbounds/core/api.ts`、`types.ts` | 页面只消费服务端 `holdFlag/currentHoldReason/canHold/canRelease/actionBlockReason/history`;扣货与解除提交当前入库版本、幂等键、必填原因和可选凭证,凭证下载只走 Firstleg 授权入口。Backend `firstleg/` 的 `InboundHoldService`、`InboundHoldGuard`、`InboundHoldEvidenceService`,文件归 `file/` | +| 入库、测量、称重库位状态、客户箱规策略与计价主数据 | 测量弹框/工作区 `modules/inbounds/measurement/InboundMeasurementDialog.vue`、`InboundMeasurementPanel.vue`,称重 `modules/inbounds/weight/components/WeightOcrPanel.vue`;主数据 `modules/fba-pricing/`、`modules/dimension-config/`、`modules/customers/components/CustomerBillingRulesPanel.vue`、`modules/fee-items/` | 客户固定四档箱规,默认计算方式为 `ADD/MULTIPLY`;测量行可选实际尺寸、箱规尺寸策略或客户默认尺寸系数,保存后以 Backend `firstleg/` 返回的外箱尺寸、体积重和计费基数为准,不在页面独立重算。费用确认的实际箱数、毛重和体积优先汇总当前 `actualMeasurementRows`,不得用可能滞后的工作区顶层合计覆盖明细。称重页在每张图片卡片只展示服务端 `locationCode/slotStatus/sharedOverflow`,不渲染或自行推导整票预留引导;删除图片须提示槽位仍归本票保留,不得按图片数组下标推导托盘或库位 | +| 一件代发托盘打印 | `modules/unified-orders/pages/UnifiedOrdersView.vue` 的“托盘打印”页签、`modules/inbounds/core/DropshipPalletPrintPanel.vue`、`core/api.ts` | 仅一件代发订单展示;测量审核预生成托盘码后由 Admin 电脑端打印,支持本票全部托盘合并打印和单托补打。PDA 只扫码组托,不承担 PDF 下载或打印机连接 | +| 入库单扣货、解除、凭证与历史 | `modules/inbounds/pages/InboundOrdersView.vue`、详情工作区、`modules/inbounds/core/api.ts` | 列表展示服务端 `holdFlag/currentHoldReason` 并支持筛选;详情只按 `canHold/canRelease/actionBlockReason` 开放操作。扣货与解除提交当前 `version`、原因和独立幂等键,可选凭证先按明确客户与入库单上传;历史文件只调用专用下载入口,不把上传成功但未提交命令的文件冒充扣货证据 | +| 客户专属价格簿、柜级阶梯运费与受控调价 | 列表/上传 `modules/fba-pricing/carrier-rates/pages/CarrierRatesView.vue`,版本详情/仓库映射/确认 `modules/fba-pricing/carrier-rates/pages/CarrierRateDetailView.vue`,路由 `modules/fba-pricing/routes.ts`;`modules/inbounds/fee/`、`modules/unified-orders/components/InboundFeeDetailManager.vue` | 列表与上传后按 `versionId` 进入 `fba-carrier-rate-detail` 独立详情页,价格行只读;回归入口 `modules/fba-pricing/carrier-rates/pages/CarrierRatesView.test.ts`。价格簿页面显式选择公共报价或具体客户;列表、上传、确认只作用于该 `customerId`,不得混合版本。计价优先消费客户专属 ACTIVE 版本,无专属版本才回退公共 ACTIVE;柜级阶梯金额和各客户分摊只展示服务端结果。调整计费量只能在服务端返回的上下 10% 范围内并提交当前费用版本、原因与二次确认;私人仓尾程派送费使用稳定 `PRIVATE_LAST_MILE_DELIVERY` 代码且只对 `PRIVATE` 目的地开放;一件代发须在预装确认前完成试算,计价后保持服务端 `WAIT_PALLETIZE` 状态 | +| 财务、客户资金账户与柜级公共费用分摊 | `modules/finance/`,继续读该目录 `AGENTS.md`;客户账户 `pages/FinanceCustomerAccountsView.vue`,柜费 `modules/loading-lists/components/ContainerFeesPanel.vue`、`api/containerFees.ts` | `finance/`、`firstleg/`、`file/`、`docs/财务管理/`;实际、冻结、可用余额、授信与流水只消费 Finance 权威结果,充值审核和手工充扣携带版本与幂等键。跨客户公共柜费只按服务端柜内客户/订单事实分摊,金额必须守恒,创建与审核、驳回、调整、作废均整组原子处理 | +| 货柜双工作台、排柜、预装/装柜清单、真实柜号后补与整柜报关 | 国内 `modules/container-loading/pages/DomesticContainersView.vue`、海外 `OverseasContainersView.vue`,共享详情实现 `ContainerDetailView.vue`;预装详情 `modules/loading-lists/pages/PreloadPlanDetailView.vue`,资料/PDA 证据 `components/ContainerOverviewPanel.vue`,报关 `components/ContainerCustomsPanel.vue` | 国内“送柜报关”只消费 `/containers/domestic` 与服务端 `allowedActions`,承载模拟柜、订舱关联、智能装柜;模拟柜创建选择与智能装柜一致的 9 类计划渠道,未关联订舱前实际渠道为空,订舱关联候选按计划渠道组匹配并同时展示订舱号与提单号、装柜清单页签、出口报关和国内出仓;创建柜次时自动生成唯一预装计划,预装详情先调用 `/loading-plans/{planId}/lifecycle`,完成后深链进入国内货柜详情的装柜清单页签,独立装柜清单导航 `/loading-lists` 复用已确认清单 API,查看进入独立详情路由 `/loading-lists/:planId`,保留返回列表和打印;独立清单详情与货柜清单页签共同使用 `useLoadingListDetail.ts` 加载、校验和处理错误,`LoadingListDetailDrawer.vue` 统一密度、摘要、明细、历史及打印入口,打印实现归 `printLoadingList.ts`,外层只保留返回目标与货柜专属 PDA/报关区域;货柜列表经 `ContainerWorkbenchView.vue` 在国内装柜与海外清关提柜之间按权限切换。订舱关联允许真实集装箱号为空,但确认预装和创建 PDA 任务前必须补齐;历史已建但缺号的任务仍可在预装详情首次补录,保存后恢复 PDA 可进入状态;全买单柜继续使用 `complete-all-buy`。海外“清关提柜”只消费 `/containers/overseas`,承载船节点、目的港清关、DO/PU、码头车队和还空;原预约/实际提柜展示位改为整柜预计/实际到仓,预计到仓在列表内人工快捷维护并经海外接口同步整柜尾程订单,实际到仓只读海外 PDA 首次开始收货时间。还空截止事实由服务端基于卸柜完成时间加 72 小时计算,前端不重算。两端统一使用 `containerBookingId`,列表范围由服务端 `scope=ACTIVE/HISTORY` 分页,禁止浏览器筛选或本地推导写资格。后端为 `LoadingListService`、`firstleg/customs/container/`、`firstleg/terminaldrayage/` 和 `lastleg-service/`,文件归 `file/` | +| 合柜报关 / 同柜不可拆组 | `modules/unified-orders/api/coLoadGroups.ts`、`pages/UnifiedOrdersView.vue`;装柜消费 `modules/container-loading/pages/SmartLoadingWorkbenchView.vue` | 前端只提交订单 ID;Backend `firstleg/loading/coload/` 展开并冻结每票全部可用入仓片段。成员数是入仓片段数,不等于订单票数;只要求同国内仓、同渠道,允许跨客户及不同目的仓港;分配、移除、取消均整组处理 | +| 全局智能装柜工作台、入仓码候选与箱数分配 | `modules/container-loading/components/SmartLoadingDialog.vue` 内嵌 `pages/GlobalSmartLoadingView.vue`,柜次页 `SmartLoadingWorkbenchView.vue`,以及 `smartLoadingChannelGroups.ts`、`smartLoadingItems.ts`、`api/platformContainerLoading.ts`;全局页当前不注册独立路由 | Backend `firstleg/` 的 `/inbound-candidates`、`/containers`、`/inbound-assignments`、`/plans/{planId}/pending-review-assignments`、`/group-assignments`、`/unpicked-items`。`orderStatuses` 最多四项且未传表示全部未完成状态;候选状态多选覆盖待审核、待测量、待分柜、装柜中,状态只用于筛选展示,按钮资格以服务端 `canAssign` 为准;待审核/待测量可提前占位但不得提前确认预装或启动 PDA。计划箱数编辑上限、已用和未用展示只消费服务端 `totalPackageCount/remainingPackageCount`,不得从候选页或本地规格重新拼算。人工加入允许跨渠道,智能推荐仍按柜次渠道。柜次容量和限重取服务端字段;同柜组作为单卡整组分页、分配和移除,任一成员已拣时整组保留 | +| 国内货柜履约订舱资料、打印、关联候选与出仓前船期 | `modules/ocean-bookings/` 的 `components/OceanBookingDocumentsPanel.vue`、`OceanBookingPrintDialog.vue`,以及 `modules/container-loading/`、`modules/port-schedules/` | 订舱草稿按当前版本维护资料,放舱后资料与表单只读;打印对草稿与已放舱订舱均开放,按当前已保存资料生成,未填写字段显示“—”。`platformOceanBookings.ts` 的 `/link-candidates` 只消费服务端分页,`containerCount` 是柜型需求 `quantity` 总和;Backend `firstleg/oceanbooking/`、`file/`、`print/`、`firstleg/portschedule/` | +| 订舱级拖车预约 | `modules/trucking/api/platformTrucking.ts` 的 `/candidates`、`components/TruckAppointmentForm.vue`、`pages/TruckAppointmentCreateView.vue`、`TruckAppointmentsView.vue` | 表单只选择服务端返回的 `RELEASED` 订舱与 `TRUCKING + truckingScope=DOMESTIC` 国内拖车供应商,不选择或提交客户、柜次、装柜计划或真实集装箱号;清关提柜固定选择 `OVERSEAS` 海外车队,类型在供应商主档单选维护;提单号和仓库由订舱冻结事实带入。一个订舱最多一张非终态拖车单,重复创建转入现有单继续编辑;PDA 完成不自动推进拖车。附件为平台作用域且不重复提单文件;Backend `TruckAppointmentService`、`truck-appointments-v1.yaml` | +| 目的港码头拖车与还空预警 | 海外清关提柜工作台/详情 `modules/container-loading/pages/OverseasContainersView.vue`、`OverseasContainerDetailView.vue`,API `modules/container-loading/api/terminalDrayages.ts`、`containerArrivalTimes.ts` | 与订舱级拖车预约是不同生命周期;按 `containerBookingId` 读取服务端状态、`allowedActions` 和历史,不能复用订舱级拖车表单或自行推进状态。列表“预计到仓”列直接提供人工日期时间选择器,保存时通过 Lastleg 柜级版本和幂等键原子更新该柜全部尾程订单,已有入库计划后禁止修改;“实际到仓”只读 `ll_receiving_container_session.receiving_at`,即海外 PDA 首次开始收货时间。还空截止、超时天/小时只展示服务端基于权威卸柜完成时间加 72 小时计算的结果;Backend `firstleg/terminaldrayage/`、`lastleg-service/`,邮件记录通过 `terminalDrayages.ts` 查询与重试。 | +| 历史报关协同与国内出仓 | `modules/customs/`、`modules/outbound-dispatch/` | `modules/customs/` 是历史报关案件/清单/代理提交与回执入口;装柜完成后的整柜退税/买单资料从货柜详情进入,不在旧模块复制;Backend `firstleg/outbound/dispatch/`、`firstleg/customs/`、`file/` | +| 主数据、下载、退避任务重试、审计 | `modules/master-data/`、`modules/downloads/pages/DownloadCenterView.vue`、`modules/audit/` | File 导出任务返回 `attemptCount/maxAttempts/nextRetryAt`;仅失败或已进入自动退避的等待任务显示重试,等待任务重试用于立即执行当前投递,不创建新任务;联查 `warehouse/`、`file/`、`audit-service/` | +| 海外接柜、入库、库存与库内服务 | `modules/overseas-warehouse/` | `gateway/`、`lastleg-service/`、`warehouse-overseas-service/`;与排车、出库、派送、异常退回和一件代发共同组成海外仓全流程 | +| 海外采购、卡车、散板 | `modules/overseas-business/` | `gateway/`、`lastleg-service/` | +| 海外仓业务合作仓与通用资料 | 合作仓 `modules/warehouses/`;报价/汇率/车辆 `modules/overseas-master-data/` | 国内 `warehouse/`、海外实际资源与服务 | +| 海外仓业务国内出仓后船节点 | `modules/port-schedules/`、`modules/ocean-bookings/`、货柜详情 | `firstleg/portschedule/`、`integration-service/`;预计节点只展示不推进实际进度,手工实际节点必须严格向后追加并防重复,十节点标准见 `../../docs/海外仓业务/船节点标准化契约.md` | +| 海外排车、出库与派送 | `modules/overseas-warehouse/AGENTS.md` | 按模块表定位排车、出库、派送页面及 API;履约边界在模块内维护 | +| 海外异常退回与客户回访 | `modules/overseas-warehouse/AGENTS.md` | 派送异常退回区别于退货库存;页面、API 与原子推进约束见模块指引 | + +全量验收按业务从 `../../docs/订单与客户/`、`入库测量与计费/`、`国内货柜履约/`、`财务管理/` 选择最小契约;海外跨模块流程或入口不明时读 `../../docs/海外仓业务/README.md`,已定位局部任务直接沿模块入口。进度、排期或验收状态问题再读取对应进度表和报告。 + +散货托盘码打印:`modules/loading-lists/components/PalletLabelPrintDialog.vue`、`modules/loading-lists/api/platformLoadingLists.ts`,由货柜详情与装柜清单详情进入;先调用 `/pallet-labels/prepare` 获取服务端按 FBA 仓分组的冻结清单,再通过 `/pallet-labels/print` 下载 PDF。托盘码使用“真实柜号-FBA 地址内部编号-三位序号”,页面同时展示 FBA 仓码和内部编号;支持全部打印或按仓指定张数补打,一件代发托盘不在该清单,前端不生成或重新编号托盘码。 + +## 海外模块内部快速索引(海外仓全流程) + +- 海外仓的尾程订单控制与柜级入仓计划转换、退货库存、排车出库、异常回访:读取 `src/modules/overseas-warehouse/AGENTS.md`,字段与流程仅在该模块维护。 + +| 关键词 | 页面/路由入口 | API 与联查 | +|---|---|---| +| 海外采购 | `modules/overseas-business/pages/PurchaseOrdersView.vue`、详情/编辑页 | `modules/overseas-business/api/platformOverseasBusiness.ts` 的 purchase 方法;Backend `lastleg-service/procurement/` | +| 美国卡车、本土散板 | `modules/overseas-business/pages/TransportOrdersView.vue`、`TransportOrderFormView.vue` | 同一 API 的 us-truck/looseboard 方法;Backend `lastleg-service/trucking/`、`looseboard/` | +| 海外报价、月度汇率、车辆 | `modules/overseas-master-data/pages/OverseasQuotesView.vue`、`ExchangeRatesView.vue`、`VehiclesView.vue` | `modules/overseas-master-data/api/overseasMasterData.ts`;Backend `lastleg-service/masterdata/` | +| 司机档案、驾驶证、到期提醒与资格预览 | `modules/overseas-master-data/pages/DriversView.vue`、`DriverDetailView.vue`;最近指引 `modules/overseas-master-data/AGENTS.md` | `api/drivers.ts`;Backend `lastleg-service/fleet/driver/`、File `FLEET_DRIVER_LICENSE` | diff --git a/src/core/navigation/adminNavigation.test.ts b/src/core/navigation/adminNavigation.test.ts index db1a7e2d..2b20c3f7 100644 --- a/src/core/navigation/adminNavigation.test.ts +++ b/src/core/navigation/adminNavigation.test.ts @@ -31,12 +31,12 @@ describe('adminNavigation', () => { { title: '货柜管理', children: ['订舱列表', '拖车列表', '货柜列表', '装柜清单', '预装清单'] }, { title: '海外尾程', children: ['尾程订单', '入库单', '废弃单', '出库单', '换标单', '派送单', '托盘管理', '库位管理', '库存管理', '退货库存管理', '退货箱号管理'] }, { title: '预约管理', children: ['预约列表'] }, - { title: '车辆管理', children: ['排车列表', '车辆列表', '司机管理', '车次管理', '拼车接货', '出车记录'] }, - { title: '海外业务', children: ['采购订单', '本土卡车', '本土散板'] }, + { title: '车辆管理', children: ['排车列表', '车辆列表', '司机管理', '出车记录'] }, + { title: '海外业务', children: ['采购订单', '本土卡车', '本土散板', '散板需求'] }, { title: '客户服务', children: ['异常订单'] }, { title: '财务管理', children: ['客户账户', '客户应收应付', '应收款统计表', '客户对账单', '结算单应收', '结算单应付'] }, { title: '海外仓', children: ['经营总览', '销量分析', '库存分析', '数据采集'] }, - { title: '通用', children: ['费用项目', '汇率管理', '仓库管理', '海外仓报价', '车队报价', '船期价格表', '下载中心'] }, + { title: '通用', children: ['费用项目', '汇率管理', '仓库管理', '海外仓报价', '车队报价', '船期价格表', '下载中心', '供应商列表'] }, { title: '系统管理', children: ['用户管理', '角色权限', '菜单权限'] }, ]) expect(visible).toHaveLength(13) @@ -196,13 +196,15 @@ describe('adminNavigation', () => { expect(getNavigationLeaves(filtered).map((item) => item.path)).toEqual(['/dashboard', '/master-data/fee-items']) }) - it('retains the supplier route permission while omitting its entry from the latest sidebar', () => { + it('shows the restored supplier entry under general only with its list permission', () => { const visible = filterAdminNavigation(adminNavigation, new Set(['platform:supplier:list'])) const missing = filterAdminNavigation(adminNavigation, new Set()) expect(getNavigationLeaves(adminNavigation).find(item => item.path === '/suppliers')?.permission).toBe('platform:supplier:list') - expect(getNavigationLeaves(visible).map((item) => item.path)).toEqual(['/dashboard']) + expect(getNavigationLeaves(visible).map((item) => item.path)).toEqual(['/dashboard', '/suppliers']) + expect(visible.find(item => item.title === '通用')?.children?.map(item => item.path)).toEqual(['/suppliers']) expect(getNavigationLeaves(missing).map((item) => item.path)).toEqual(['/dashboard']) + expect(getNavigationLeaves(filterAdminNavigation(adminNavigation, new Set(['platform:supplier:create', 'platform:supplier:edit']))).map(item => item.path)).toEqual(['/dashboard']) }) it('shows split container workbenches without a standalone smart-loading menu', () => { diff --git a/src/core/navigation/adminNavigation.ts b/src/core/navigation/adminNavigation.ts index 65b13116..0b67565c 100644 --- a/src/core/navigation/adminNavigation.ts +++ b/src/core/navigation/adminNavigation.ts @@ -134,8 +134,7 @@ export const adminNavigation: AdminNavigationNode[] = [ { key: 'fba-dispatch', title: '排车列表', icon: Truck, path: '/overseas-warehouse/dispatches', permission: 'platform:overseas-warehouse:dispatch:list' }, { key: 'vehicle-list', title: '车辆列表', icon: Truck, path: '/overseas-master-data/vehicles', permission: 'platform:overseas-master-data:vehicle:list' }, { key: 'driver-management', title: '司机管理', icon: ContactRound, path: '/overseas-master-data/drivers', permission: 'platform:overseas-master-data:driver:list' }, - { key: 'fleet-trips', title: '车次管理', icon: Truck, path: '/overseas-master-data/trips', permission: 'platform:fleet:trip:list' }, - { key: 'fleet-capacity-offers', title: '拼车接货', icon: Truck, path: '/overseas-master-data/capacity-offers', permission: 'platform:fleet:capacity-offer:list', permissionAnyOf: ['platform:fleet:capacity-offer:manage'] }, + { key: 'fleet-trips', title: '车次管理', icon: Truck, path: '/overseas-master-data/trips', permission: 'platform:fleet:trip:list', hidden: true }, { key: 'fleet-operations', title: '出车记录', icon: Truck, path: '/overseas-master-data/trip-operations', permission: 'platform:fleet:operations:list' }, ], }, @@ -147,6 +146,7 @@ export const adminNavigation: AdminNavigationNode[] = [ { key: 'overseas-purchase', title: '采购订单', icon: ClipboardList, path: '/overseas-business/purchases', permission: 'platform:overseas-business:purchase:list' }, { key: 'overseas-truck', title: '本土卡车', icon: Truck, path: '/overseas-business/trucks', permission: 'platform:overseas-business:truck:list' }, { key: 'overseas-looseboard', title: '本土散板', icon: ClipboardList, path: '/overseas-business/looseboards', permission: 'platform:overseas-business:looseboard:list' }, + { key: 'fleet-capacity-offers', title: '散板需求', icon: Truck, path: '/overseas-master-data/capacity-offers', permission: 'platform:fleet:capacity-offer:list', permissionAnyOf: ['platform:fleet:capacity-offer:manage'] }, ], }, { @@ -204,7 +204,7 @@ export const adminNavigation: AdminNavigationNode[] = [ { key: 'fleet-quotes', title: '车队报价', icon: Truck, path: '/overseas-master-data/fleet-quotes', permission: 'platform:fleet:quote:list' }, { key: 'fba-carrier-rates', title: '船期价格表', icon: CircleDollarSign, path: '/first-leg-pricing/carrier-rates', permission: 'firstleg:fba-pricing:rate:view' }, { key: 'download-center', title: '下载中心', icon: Download, path: '/downloads', permission: 'platform:download:list' }, - { key: 'supplier-list', title: '供应商列表', icon: ContactRound, path: '/suppliers', permission: 'platform:supplier:list', hidden: true }, + { key: 'supplier-list', title: '供应商列表', icon: ContactRound, path: '/suppliers', permission: 'platform:supplier:list' }, { key: 'express-companies', title: '快递公司', icon: Truck, path: '/master-data/express-companies', permission: 'platform:express:list', hidden: true }, { key: 'counterparties', title: '往来单位', icon: ContactRound, path: '/master-data/counterparties', permission: 'platform:counterparty:list', hidden: true }, { key: 'mojia-sku-sync', title: '称重机器', icon: Database, path: '/general/mojia-sku-sync', permission: 'platform:mojia-sku-sync:list', hidden: true }, diff --git a/src/modules/appointment-calendar/PROTOTYPE_MATRIX.md b/src/modules/appointment-calendar/PROTOTYPE_MATRIX.md index 27b6b138..6264dbee 100644 --- a/src/modules/appointment-calendar/PROTOTYPE_MATRIX.md +++ b/src/modules/appointment-calendar/PROTOTYPE_MATRIX.md @@ -30,10 +30,18 @@ 本次发布单元为Admin、Firstleg、Lastleg;新增两个只读日历接口复用现有Gateway通配路由,日历功能不新增数据库结构。环境发布与真实HTTP结果只维护在`docs/测试环境单机部署与配置契约.md`。人工视觉待用户在1920×1080、100%缩放下截图确认。 +2026-10-16 本次 UI 检查:用户重新要求检查后,运行 Admin 全量测试,397 文件 / 3292 项通过;发现并修正 QueryToolbar 必填 `ariaLabel` 属性的模板写法。补充真实 KeepAlive + Teleport 离开/恢复及点击提醒弹层回归后,最新定向检查 3 文件 / 66 项通过,最终 `pnpm typecheck`、`pnpm build` 和 `git diff --check` 通过。构建仍有既有 AnalyticsChart 约 588 kB 分块提示。此记录更新下文历史“未运行验证”的结论;本地自动检查不代表测试环境或人工视觉验收。所属客户显示缺项仍待后端合同补齐,INP 采集报错的脚本来源未确认,本次未部署。日志为 `.logs/appointment-ui-current-{test,focused,typecheck,build}.log`。 + 本轮依据:`C:/Users/BY-OY/.codex/visualizations/2026/10/14/01a13a8d-e968-7962-906b-1a514792d921/appointment-calendar-guided.html`(V13,SHA-256 `61902e410be755aa9cbf9fcfaa9d393bfa49dd8d592cd8773af5396e78c29836`)及 `docs/海外仓业务/预约工作日历V13变更说明.md`、开发协作契约。保留既有日历和居中弹框优化,不改变原业务实际执行入口。 本次密度调整依据:用户提供的主页面截图 `codex-clipboard-93984795-68cb-4082-96b3-c83189d8a357.png`、展开明细截图 `codex-clipboard-a98edd3b-82af-4f1b-989e-7d0f028e0965.png`,以及用户确认的压缩留白、日期定位、控件统一、零值弱化和统计范围说明。 +2026-10-16 操作列样式核对:用户截图 `codex-clipboard-9d54345c-c9c1-4991-82ba-b319948f781b.png` 明确要求与外层表格统一。 + +| 页面 | 操作 | 截图依据与布局 | 当前实现 | 本轮结论 | +|---|---|---|---|---| +| 预约工作明细表格 | 查看详情或服务端提供的下一步操作 | 操作列内蓝色普通文字,无边框、底色或装饰图标 | `RowActions` 默认描边样式 | 使用公共 `text-actions` 模式,保留当前文案、权限、点击承载和长文案换行;待人工截图确认 | + 2026-10-16 当前承载调整:用户反馈“点击外面关闭不了,也看不清”。日期/类型/全局明细、事项详情、原侧边编辑表单和到港参考统一改用带遮罩的居中 `AppDialog`,常规宽度 1120px、高度 88dvh,支持全屏;标题与分页固定、仅正文滚动。遮罩、Esc 和关闭按钮统一经过未保存/提交结果待确认保护。此结论优先于下表早期侧边承载描述;原业务独立页面跳转、确认流程和统计口径保持原样。改后待人工截图确认。 V13 日历布局:按已确认定高要求,月日格固定 146px,周日格固定 270px;只展示五类服务端计数和独立到港参考,不追加逐事项卡片、不依明细条数扩高。替代下表早期“随内容自然增高”的结论;不裁掉五类入口,仍需人工核对 1920×1080、100% 下完整内容。 @@ -71,6 +79,7 @@ V13 日历布局:按已确认定高要求,月日格固定 146px,周日格 | V13 场景 | 当前前端与直接检查 | 依赖/剩余验收 | |---|---|---| +| 事项列表统一表格 | 用户截图 `codex-clipboard-1bde1991-b226-4956-8530-6de44074f818.png` 明确要求统一表格或卡片,不能混排 | 所有日期/类型/全局待办列表统一使用 DataTable 与 RowActions,替代早期多行卡片排版。操作固定最左;事项、类型、状态、作业时间、时区、流程、地点、负责人、下次跟进、提醒分列,时间保留实际/确认/目标口径。列表弹框加宽,表头与分页由共享表格承载;保留每页 8 项、筛选和返回上下文。当前调整覆盖旧卡片与独立分页结论;未运行验证或部署,待人工截图确认 | | 01 整客户 160 移出续办 | `WorkDetailPanel.test.ts` 保存后读取主要下一步;`WorkExistingDispatchForm.test.ts` 沿用真实排车单继续安排本批车辆 | 单客户移出命令、当前数量/车辆版本、原车 120 保留结果及后续目标;真实发车签收待集成 | | 02 100 先送、60 后送 | `AppointmentBatchForm.test.ts` 保留普通分批与来源版本;`WorkDetailPanel.test.ts` 区分未定作业日和跟进日 | 已确认预约/装载关系的部分移出与调整回复语义,不能以普通分批代替 | | 03 后送 60 加另一客户 40 共车 | 本批与另一客户按实际托盘各建排车单;结果页“与同仓其他排车单共车”沿业务号核对两单后复用 TripPreparationDialog/Fleet prepare,可加入已有计划车次;WorkDispatchForm、WorkSharedTripForm、TripPreparationDialog、trips API 测试覆盖传递与导航 | 实物须能对应本次数量,不能对应时先走原库存拆托;共车后的路线、容量、审核及发车须真实 HTTP 验收,非任意箱级拆托开发缺口 | @@ -172,8 +181,12 @@ V13 日历布局:按已确认定高要求,月日格固定 146px,周日格 | 本次操作 | 证据与现状 | 调整结论 | |---|---|---| +| 未定日期范围内搜索 | 用户截图 `codex-clipboard-07b5ab03-e467-4ce1-8410-13d05ac5bc51.png` 反馈大量数据缺少搜索,且测试环境仍显示旧卡片 | 全局待办弹框接入 QueryToolbar,支持柜号、批次号、车次号、地点关键词;沿现有 items API 同时传 view(含 UNDATED)与 keyword,服务端全范围筛选后分页,不过滤当前 8 条。搜索/清空回第 1 页,返回明细保留搜索词和分页,原业务跳转的 return URL 保留 workListKeyword。保留全局统计口径;表格与搜索均为当前工作区改动,本任务未部署,未运行验证 | +| 所属客户缺失排查 | 用户截图 `codex-clipboard-645f5354-0dfd-4b90-b6f7-05ff9b1f2ffb.png`;`BusinessReference` 只有 kind/key/businessNo/label,客户引用存在 `CUSTOMER + 内部客户键 + null + 所属客户` 的生产路径,详情查询仅合并引用 | 前端按 `businessNo || '—'` 展示,缺少真实客户名称;不能据此认定没有关联客户,也不能回退内部客户键。待工作事项后端按权威客户目录补充名称/业务编号并明确展示合同;遵循开发协作契约,本任务不保留后端改动。未完成接口或环境验收 | +| 提醒列与跳转遮罩 | 用户截图 `codex-clipboard-54e0b56d-448c-40da-a147-15798c932091.png` 和按钮反馈 | 提醒列固定 105px,仅显示去重后的条数,点击展开完整可滚动提醒,不撑高表格行。预约页被 KeepAlive 停用时卸载挂到 body 的弹框,恢复时保留父级事项/分页上下文。用户 startTime 截图实际是 INP 采集的空 entries 读取,脚本来源尚待确认,不能将该报错认定为已修复;本轮未运行验证或部署 | +| 事项详情的信息层级 | 用户 2026-10-16 截图 `codex-clipboard-cad4abc6-51d1-42f2-8e7b-7914b264348e.png` 反馈排版不清楚、缺少重点 | 顶部突出事项名称与状态;作业时间、时区、地点、负责人和下次跟进复用共享描述表;原有提醒去重后集中展示,保留完成态与跟进日期语义;当前操作与关联资料/历史分区,保留所有动作资格、原业务跳转、默认历史展开条件和未保存保护。空原业务进度不渲染空标题;已知事件代码使用中文名称,未知代码保留在辅助信息中,时间保留原时区。本轮未运行验证或部署,待人工截图确认 | | 列表信息层级与日历视觉细化 | 用户 2026-10-16 截图 `codex-clipboard-6858cde3-eda3-4a02-a043-e1a47143f67c.png`、`codex-clipboard-5a68965d-cdbd-4ce9-b8b3-385d9d76b4f2.png`,并明确要求按建议优化 | 查看详情/历史使用普通描边按钮,办理动作保留主色且仍在左侧;完整标题、状态、作业时间、时区、地点、流程、负责人、跟进日期及异常分层显示。日历类型改浅色底与弱边框,保留语义色、今天/选中态;周末表头与到港参考保持辅助层级,零值弱化、超期数字提示。保留现有覆盖面板、字号、统计和动作链;改后待人工截图确认 | -| 工作明细左侧长操作按钮 | Admin #621 真实 1920×1080 截图 `baiyue-firstleg/reports/appointment-browser-20261015/admin-621-follow-today-1920.png`;按钮与文本实际行高仅 13px | 保留完整文案和 108px 操作列,统一使用 20px 行高、自然换行和自适应高度;普通/展开面板及主操作/查看详情使用同一规则 | +| 工作明细左侧长操作按钮 | 用户 2026-10-16 截图 `codex-clipboard-b36b905a-a223-4fa6-98ca-7ece08149a37.png` 反馈按钮过大;原 108px 列宽、全宽实心按钮导致长文案换行撑高 | 操作列调整为 184px,按钮按文案宽度使用共享 `density-action` 描边样式和 28px 最小高度;完整保留服务端动作标签及点击行为,超长文案仍可换行。业务编号使用深色标题,辅助信息使用说明字号,行间距收紧。适用全部工作明细弹框及全屏态,覆盖早期实心主按钮结论;改后待人工截图确认,本轮未运行验证或部署 | | 流程与工具栏分组 | 用户后续流程下拉、日期工具栏及上下操作区截图(`115df974`、`d217350b`、`80dd9fa1`) | 流程改为四个直接切换按钮;全局待办、流程、日期与类型筛选收进连续白底工作区,各行靠左,保留全局与区间统计边界 | | 日历浏览与日期选择 | 主页面截图;原型 `renderCalendar()`、`.day.selected`、`.todaymark` | 缩小日格空白,今天与选择态分别标识;零项日期只显示日期;显示实际查询起止日期,保留加载失败状态 | | 日期/类型/全局明细 | 展开截图中日历被挤窄;原型第 11 行 `.workspace`、`.panel` 为右侧覆盖布局 | 恢复覆盖式侧边工作面板,不压缩日历;增加展开/收起,标题和关闭固定,正文滚动,分页固定;仍保留左侧业务操作 | @@ -197,10 +210,8 @@ V13 日历布局:按已确认定高要求,月日格固定 146px,周日格 | 客户仓批次用车 | 原排车与车次安排入口 | 按 DISPATCH 引用读取原排车单,再进入原车次安排 | `WorkExistingDispatchForm.vue`;核对不可变批次键与真实业务号,保留原实物;不调用 FBA 候选,不新建排车单;原派送进度按明确业务引用进入 | | 本柜接仓计划与记录 | `executeForm()`、原入库单详情 | 单计划进入对应客户原单,多计划列出本柜实际关联清单;缺计划时先预览整柜分组 | `WorkReceiptPlans.vue`;复用 container-summaries、原整柜预览/生成 API 和原入库单路由,快照与原资源重试键保持一致;实际收货仍由原仓库作业流程登记 | | 失败、无权限、未保存 | 真实接口与应用验收规则 | 明确失败和重试;错误不改为0项,保留输入和原上下文 | 各组件直接回归;未知命令不执行,未确认保存结果禁止离开 | - | 保存后工作清单同步等待 | 用户截图 `codex-clipboard-40520e09-5cbf-489f-9941-325bdf22bdea.png`,顶部保存成功、正文续办 A0409 | 保留当前事项弹框和保存结果,不改变承载方式 | `useWorkSyncRead.ts` 仅对当前完整工作清单同步守卫重读 GET;间隔2秒,累计等待最多30秒,超限显示原错误;日历、汇总、明细与批次续办一致,不重复写入 | | 查看原排车单、继续安排本批车辆 | 用户截图 `codex-clipboard-4791b73f-7a9f-4be5-910a-38911e135ef4.png`、`codex-clipboard-e5b41c4b-a00c-4b99-ac06-cd23ec910aa9.png` | 原排车单进入正式路由,事项弹框随缓存页停用而移除;继续配车沿目标事项读取资格 | 保留已有 `pageActive/onDeactivated` 修复;跨事项 `nextAction` 仅导航,目标事项重新核对 `allowedActions`,不继承送仓资格或执行旧动作。修复后待人工截图确认 | - | 刷新后沿用已有FBA排车单 | 当前对话指定沿用原DS;`WorkExistingDispatchForm.vue` 的既有原排车列表和 `TripPreparationDialog` | 保留事项弹框,显示本批原排车单;继续用原安排车次弹框,不重复选托盘或创建 | `WorkDispatchForm.vue` 先读当前批次 `receiving.transport`,再按原排车单 `appointmentBatches` 校验批次/客户;已绑定车次直接查看,关系读取失败不显示创建入口;剩余实物仅显式选择后读取。待人工截图确认 | 视觉目标:1920×1080、100%缩放,待人工截图确认。单元测试与类型检查单独报告,不代表真实接口或页面验收通过。原业务跳转缺少的业务号/客户范围在协作交接中列明,不由前端补造;工作 Slot 与原预约编辑均保留完整跨日时间。原业务页面返回入口只接受预约工作页,并保留原事项、前置事项、筛选及页码。 diff --git a/src/modules/appointment-calendar/TripAppointmentPanel.test.ts b/src/modules/appointment-calendar/TripAppointmentPanel.test.ts index 9f5298cb..f4e8b8b9 100644 --- a/src/modules/appointment-calendar/TripAppointmentPanel.test.ts +++ b/src/modules/appointment-calendar/TripAppointmentPanel.test.ts @@ -10,7 +10,7 @@ vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ vi.mock('@/modules/overseas-warehouse', async original => ({ ...await original(), getDispatch: vi.fn(), getFbaAppointment: vi.fn() })) vi.mock('./batchApi', async original => ({ ...await original(), getAppointmentBatch: vi.fn() })) vi.mock('./receivingApi', async original => ({ ...await original(), getReceivingContext: vi.fn() })) -const reference = (batchKey = 'FBA_APPOINTMENT:1', batchNo: string | null = 'AP-001'): DispatchBatchReference => ({ batchKey, batchNo, customerId: 'customer-a', sources: [{ sourceKey: 'ORDER:1', destinationKey: 'FBA:ONT8', packageCount: 70 }] }) +const reference = (batchKey = 'FBA_APPOINTMENT:1', batchNo: string | null = 'AP-001'): DispatchBatchReference => ({ batchKey, batchNo, recordVersion: 0, customerId: 'customer-a', sources: [{ sourceKey: 'ORDER:1', destinationKey: 'FBA:ONT8', packageCount: 70 }] }) function render(overrides: Partial['$props']> = {}) { return mount(TripAppointmentPanel, { props: { dispatches: [{ dispatchNo: 'DS-A', customerId: 'customer-a', packageCount: 70, appointmentBatches: [reference()] }], previousDispatches: [], historyUnavailableReason: null, canWrite: true, ...overrides }, global: { stubs: { ElButton: { props: ['disabled'], template: '' }, AppDialog: { props: ['modelValue'], template: '
' }, diff --git a/src/modules/appointment-calendar/WorkDetailPanel.vue b/src/modules/appointment-calendar/WorkDetailPanel.vue index 0d752e97..ee77b2db 100644 --- a/src/modules/appointment-calendar/WorkDetailPanel.vue +++ b/src/modules/appointment-calendar/WorkDetailPanel.vue @@ -21,6 +21,14 @@ import { useWorkSyncRead } from './useWorkSyncRead' const props = defineProps<{ itemKey: string; initialAction?: WorkAction | null; canLeave: LeaveGuard }>() const emit = defineEmits<{ open: [key: string, action?: WorkAction]; external: [path: string]; saved: [result?: WorkCommandResult, command?: WorkCommand['command']]; updated: []; container: [id: string, label: string] }>() const detail = ref(null), loading = ref(false), error = ref(null), action = ref(null) +const attentionReasons = computed(() => [...new Set([ + ...(detail.value?.item.blockReasons || []), + ...(detail.value?.item.missingFields || []), + ...(detail.value?.item.overdueReasons || []), +].map(workReason))]) +const needsFollowup = computed(() => !!detail.value && detail.value.item.executionStatus !== 'COMPLETED' && (!detail.value.item.owner || !detail.value.item.followupDate)) +const historyLabels: Record = { BASELINE_OBSERVED: '同步原业务状态', GENERATED: '创建工作事项', CONFIRM_ROUTE: '确认路线', SAVE_FOLLOWUP: '记录跟进', RESOLVE_REVIEW: '处理复核', SAVE_REQUIREMENT: '保存预约要求', HANDOFF_RETURN: '交接还空责任', ACCEPT_RETURN: '确认接手还空' } +function historyTime(value: string) { return value.replace('T', ' ').replace(/\.\d+(?=Z|[+-]\d{2}:\d{2}$)/, '').replace(/Z$/, ' UTC') } const receivingOwners = computed(() => [detail.value?.item.owner, ...(detail.value?.relatedItems.map(item => item.owner) || [])].filter((person): person is { id: string; name: string } => !!person?.id)) const actionNotice = ref('') const saveCompleted = ref(false) @@ -105,17 +113,33 @@ onMounted(async () => { onBeforeUnmount(() => { ++request; controller?.abort() })