diff --git a/AGENTS.md b/AGENTS.md index aba261cf..57371fb7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,6 +44,7 @@ | 启动与懒加载资源恢复 | `main.ts`、`core/routing/navigationRecovery.ts` | 仅识别动态模块/Chunk 资源过期;30 秒内同目标最多自动恢复一次,存在未保存编辑时必须先确认 | | 工作台与指标 | `modules/dashboard/` | 指标所属模块与服务端,不复制领域契约 | | 船期日历 | `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/`;公司联系电话只作企业联系信息,不修改管理员/成员登录手机号且不要求跨企业唯一。审核必填资质仅营业执照与统一社会信用代码;法人身份证选填,可随资质包提交,也可在已有营业执照后单独补传或替换 | diff --git a/output/appointment-calendar/final-month.png b/output/appointment-calendar/final-month.png new file mode 100644 index 00000000..75a0c94e Binary files /dev/null and b/output/appointment-calendar/final-month.png differ diff --git a/output/appointment-calendar/month-five-customers.png b/output/appointment-calendar/month-five-customers.png new file mode 100644 index 00000000..7fe86be1 Binary files /dev/null and b/output/appointment-calendar/month-five-customers.png differ diff --git a/output/appointment-calendar/verification.json b/output/appointment-calendar/verification.json new file mode 100644 index 00000000..d07ba16f --- /dev/null +++ b/output/appointment-calendar/verification.json @@ -0,0 +1,78 @@ +{ + "date": "2026-10-09", + "testOnly": true, + "deployment": { + "admin": { + "build": 531, + "result": "SUCCESS", + "commit": "2caeb12" + }, + "firstleg": { + "build": 488, + "result": "SUCCESS", + "commit": "e94271706ce7" + }, + "lastleg": { + "build": 234, + "result": "SUCCESS", + "commit": "7fd7699f6e83" + } + }, + "databaseReset": { + "tables": [ + "ll_inbound_fba_appointment", + "ll_inbound_fba_appointment_item" + ], + "beforeCounts": [ + 49, + 59 + ], + "afterResetCounts": [ + 0, + 0 + ] + }, + "httpPassed": [ + "five_customer_one_appointment", + "same_command_replay", + "duplicate_cargo_conflict", + "update_version", + "stale_update_conflict", + "stale_delete_conflict", + "delete_and_calendar_absence", + "rebook_after_delete" + ], + "uiPassed": [ + "1920x1080_month", + "five_customer_detail", + "edit_saved", + "week_view", + "candidate_existing_appointment_disabled", + "two_customer_selection", + "cross_page_selection_retained", + "create_saved", + "final_month_top_alignment" + ], + "initialCalendarRequests": [ + { + "type": "appointments", + "count": 1, + "durationMs": 59 + }, + { + "type": "arrivals", + "count": 1, + "durationMs": 106 + } + ], + "pending": [ + "merchant_identity_isolation: configured login returned HTTP401 A0401", + "week_alignment_final_browser_recheck", + "browser_delete_confirmation_flow" + ], + "screenshots": [ + "month-five-customers.png", + "week-five-customers.png", + "final-month.png" + ] +} \ No newline at end of file diff --git a/output/appointment-calendar/week-five-customers.png b/output/appointment-calendar/week-five-customers.png new file mode 100644 index 00000000..7a5f0f88 Binary files /dev/null and b/output/appointment-calendar/week-five-customers.png differ diff --git a/src/app/router/index.ts b/src/app/router/index.ts index de5e877a..9fe2ee6e 100644 --- a/src/app/router/index.ts +++ b/src/app/router/index.ts @@ -33,6 +33,7 @@ import { overseasBusinessRoutes } from '@/modules/overseas-business/routes' import { overseasMasterDataRoutes } from '@/modules/overseas-master-data/routes' import { pdaReleaseRoutes } from '@/modules/pda-releases/routes' import { sailingCalendarRoutes } from '@/modules/sailing-calendar/routes' +import { appointmentCalendarRoutes } from '@/modules/appointment-calendar/routes' const router = createRouter({ history: createWebHistory(), @@ -79,6 +80,7 @@ const router = createRouter({ ...iamRoutes, ...pdaReleaseRoutes, ...sailingCalendarRoutes, + ...appointmentCalendarRoutes, { path: '403', name: 'forbidden', diff --git a/src/core/navigation/adminNavigation.test.ts b/src/core/navigation/adminNavigation.test.ts index 878b744b..b1dbf3b6 100644 --- a/src/core/navigation/adminNavigation.test.ts +++ b/src/core/navigation/adminNavigation.test.ts @@ -22,6 +22,7 @@ describe('adminNavigation', () => { { title: '国内头程', children: ['订单列表', '订单测量', '测量数据审核', '库位管理', '托盘管理', '整柜业务', '船期管理'] }, { title: '货柜管理', children: ['订舱列表', '拖车列表', '货柜列表', '装柜清单', '预装清单'] }, { title: '海外尾程', children: ['尾程订单', '入库单', '废弃单', '出库单', '换标单', '派送单', '托盘管理', '库位管理', '库存管理', '退货库存管理', '退货箱号管理'] }, + { title: '预约管理', children: ['预约列表'] }, { title: '车辆管理', children: ['排车列表', '车辆列表', '司机管理', '车次管理', '拼车需求池', '出车记录'] }, { title: '海外业务', children: ['采购订单', '本土卡车', '本土散板'] }, { title: '客户服务', children: ['异常订单'] }, @@ -29,8 +30,8 @@ describe('adminNavigation', () => { { title: '通用', children: ['费用项目', '汇率管理', '仓库管理', '海外仓报价', '车队报价', '船期价格表', '下载中心'] }, { title: '系统管理', children: ['用户管理', '角色权限', '菜单权限'] }, ]) - expect(visible).toHaveLength(11) - expect(visible.flatMap(node => node.children ?? [])).toHaveLength(51) + expect(visible).toHaveLength(12) + expect(visible.flatMap(node => node.children ?? [])).toHaveLength(52) expect(adminNavigation.find((item) => item.key === 'notifications')).toBeUndefined() }) diff --git a/src/core/navigation/adminNavigation.ts b/src/core/navigation/adminNavigation.ts index 93a6a991..4fd3b351 100644 --- a/src/core/navigation/adminNavigation.ts +++ b/src/core/navigation/adminNavigation.ts @@ -118,6 +118,14 @@ export const adminNavigation: AdminNavigationNode[] = [ { key: 'return-cartons-list', title: '退货箱号管理', icon: WarehouseIcon, path: '/overseas-warehouse/return-cartons', permission: 'platform:overseas-warehouse:return-cartons:list' }, ], }, + { + key: 'appointment-management', + title: '预约管理', + icon: CalendarDays, + children: [ + { key: 'appointment-calendar', title: '预约列表', icon: CalendarDays, path: '/appointment-calendar', permission: 'platform:appointment:list' }, + ], + }, { key: 'vehicle-management', title: '车辆管理', diff --git a/src/modules/appointment-calendar/AppointmentCalendarPanel.test.ts b/src/modules/appointment-calendar/AppointmentCalendarPanel.test.ts new file mode 100644 index 00000000..141ca57d --- /dev/null +++ b/src/modules/appointment-calendar/AppointmentCalendarPanel.test.ts @@ -0,0 +1,53 @@ +import { mount } from '@vue/test-utils' +import { describe, expect, it } from 'vitest' +import AppointmentCalendarPanel from './AppointmentCalendarPanel.vue' + +describe('appointment calendar interactions', () => { + const render = () => mount(AppointmentCalendarPanel, { + props: { initialDate: '2026-09-24', canCreate: true, events: [ + { key: 'a', type: 'delivery', date: '2026-09-24', time: '09:00-11:00', title: 'ONT8 预约送货', fields: [], canEdit: true, canDelete: true }, + ] }, + global: { stubs: { ElButton: { template: '' } }, directives: { loading: {} } }, + }) + + it('selects an already loaded date without requesting data again', async () => { + const wrapper = render() + await wrapper.get('[aria-label="2026-09-25,0 项安排"]').trigger('click') + expect(wrapper.emitted('range')).toBeUndefined() + expect(wrapper.get('[aria-label="当天安排"]').text()).toContain('当天暂无到港或送仓预约') + }) + + it('requests exactly one range for a view switch and navigates seven days', async () => { + const wrapper = render() + await wrapper.get('[aria-label="预约视图"]').findAll('button')[1]!.trigger('click') + expect(wrapper.emitted('range')).toEqual([[{ from: '2026-09-21', to: '2026-09-28' }]]) + await wrapper.get('[aria-label="下一周"]').trigger('click') + expect(wrapper.emitted('range')?.at(-1)).toEqual([{ from: '2026-09-28', to: '2026-10-05' }]) + }) + + it('keeps busy days to three previews in both views and exposes the full count', async () => { + const wrapper = render() + await wrapper.setProps({ events: Array.from({ length: 233 }, (_, index) => ({ + key: `arrival-${index}`, type: 'arrival' as const, date: '2026-09-24', time: '09:00', + title: `船舶 ${index}`, fields: [], + })) }) + for (const view of ['month', 'week']) { + if (view === 'week') await wrapper.get('[aria-label="预约视图"]').findAll('button')[1]!.trigger('click') + const day = wrapper.get('[aria-label="2026-09-24,233 项安排"]') + expect(day.findAll('.event-chip')).toHaveLength(3) + expect(day.attributes('title')).toContain('共 233 条安排') + expect(day.get('.event-more').text()).toBe('还有 230 条') + expect(wrapper.findAll('.detail-event')).toHaveLength(233) + } + }) + + it('uses the selected day for creation and emits edit/delete only from detail actions', async () => { + const wrapper = render() + await wrapper.findAll('button').find(button => button.text() === '+ 新增预约')!.trigger('click') + expect(wrapper.emitted('create')).toEqual([['2026-09-24']]) + await wrapper.findAll('button').find(button => button.text() === '编辑')!.trigger('click') + await wrapper.findAll('button').find(button => button.text() === '删除')!.trigger('click') + expect(wrapper.emitted('edit')?.[0]?.[0]).toMatchObject({ key: 'a' }) + expect(wrapper.emitted('delete')?.[0]?.[0]).toMatchObject({ key: 'a' }) + }) +}) diff --git a/src/modules/appointment-calendar/AppointmentCalendarPanel.vue b/src/modules/appointment-calendar/AppointmentCalendarPanel.vue new file mode 100644 index 00000000..ad532175 --- /dev/null +++ b/src/modules/appointment-calendar/AppointmentCalendarPanel.vue @@ -0,0 +1,164 @@ + + + + + diff --git a/src/modules/appointment-calendar/AppointmentCalendarView.test.ts b/src/modules/appointment-calendar/AppointmentCalendarView.test.ts new file mode 100644 index 00000000..60a884b4 --- /dev/null +++ b/src/modules/appointment-calendar/AppointmentCalendarView.test.ts @@ -0,0 +1,69 @@ +import { flushPromises, mount } from '@vue/test-utils' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import AppointmentCalendarView from './AppointmentCalendarView.vue' +import { createFbaAppointment, listFbaAppointmentCalendar, listInboundOrders } from '@/modules/overseas-warehouse' +import { listArrivalCalendar } from './api' + +vi.mock('./api', () => ({ listArrivalCalendar: vi.fn(), deleteAppointment: vi.fn() })) + +vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) })) +vi.mock('@/modules/overseas-warehouse', () => ({ + createFbaAppointment: vi.fn(), getFbaAppointment: vi.fn(), listFbaAppointmentCalendar: vi.fn(), + listInboundOrders: vi.fn(), updateFbaAppointment: vi.fn(), + overseasWarehousePermissions: { inboundOrderAppointment: 'write' }, +})) +vi.mock('element-plus', () => ({ ElMessage: { success: vi.fn(), warning: vi.fn() }, ElMessageBox: { confirm: vi.fn() } })) + +const candidates = [ + { lineId: '9007199254740993', customerId: 'A', fbaWarehouseCode: 'ONT8', planId: '1', planNo: 'IN-A', orderId: '10' }, + { lineId: '9007199254740994', customerId: 'B', fbaWarehouseCode: 'ONT8', planId: '2', planNo: 'IN-B', orderId: '20' }, +] +function render() { + return mount(AppointmentCalendarView, { global: { directives: { loading: {} }, stubs: { + AdminPage: { template: '
' }, + AppointmentCalendarPanel: { template: '' }, + AppDialog: { props: ['modelValue'], template: '
' }, + ElForm: { template: '
' }, + ElFormItem: { template: '' }, + ElInput: { props: ['modelValue', 'placeholder'], template: '' }, + ElDatePicker: { props: ['modelValue'], template: '' }, + ElButton: { template: '' }, + ElSelect: true, ElOption: true, + DataTable: { props: ['data'], template: '
' }, + LoadErrorState: true, ApiErrorMeta: true, + } } }) +} + +describe('appointment calendar data integration', () => { + beforeEach(() => { + vi.resetAllMocks() + vi.mocked(listFbaAppointmentCalendar).mockResolvedValue([]) + vi.mocked(listArrivalCalendar).mockResolvedValue([]) + vi.mocked(listInboundOrders).mockResolvedValue({ records: candidates, total: 2, page: 1, size: 10, pages: 1 }) + vi.mocked(createFbaAppointment).mockResolvedValue({ version: 0, id: '1', appointmentNo: 'APT-AB', appointmentAt: '2026-10-09T09:00:00', expectedLoadingAt: '2026-10-09T07:00:00', appointmentStatus: 'PENDING', selectedCount: 2, fbaWarehouseCode: 'ONT8', items: [] }) + }) + + it('creates one shared appointment for two customers without sending a fabricated header customer', async () => { + const wrapper = render() + await flushPromises() + expect(listFbaAppointmentCalendar).toHaveBeenCalledTimes(1) + expect(listArrivalCalendar).toHaveBeenCalledTimes(1) + expect(listInboundOrders).not.toHaveBeenCalled() + await wrapper.get('[data-create]').trigger('click') + await wrapper.get('input[placeholder="例如 ONT8"]').setValue('ONT8') + await wrapper.findAll('button').find(button => button.text() === '查询可预约货物')!.trigger('click') + await flushPromises() + await wrapper.get('[data-select]').trigger('click') + await wrapper.get('input[placeholder="请输入预约编号"]').setValue('APT-AB') + await wrapper.findAll('[data-date]')[2]!.setValue('2026-10-09T07:00:00') + await wrapper.get('[data-save]').trigger('click') + await flushPromises() + expect(createFbaAppointment).toHaveBeenCalledTimes(1) + expect(createFbaAppointment).toHaveBeenCalledWith(expect.objectContaining({ + appointmentNo: 'APT-AB', lineIds: ['9007199254740993', '9007199254740994'], + })) + expect(vi.mocked(createFbaAppointment).mock.calls[0]![0]).not.toHaveProperty('customerId') + expect(listInboundOrders).toHaveBeenCalledTimes(1) + expect(listFbaAppointmentCalendar).toHaveBeenCalledTimes(2) + }) +}) diff --git a/src/modules/appointment-calendar/AppointmentCalendarView.vue b/src/modules/appointment-calendar/AppointmentCalendarView.vue new file mode 100644 index 00000000..15a1939a --- /dev/null +++ b/src/modules/appointment-calendar/AppointmentCalendarView.vue @@ -0,0 +1,252 @@ + + + + + diff --git a/src/modules/appointment-calendar/PROTOTYPE_MATRIX.md b/src/modules/appointment-calendar/PROTOTYPE_MATRIX.md new file mode 100644 index 00000000..c5095740 --- /dev/null +++ b/src/modules/appointment-calendar/PROTOTYPE_MATRIX.md @@ -0,0 +1,19 @@ +# 预约管理交互矩阵 + +原型:用户提供 `C:/Users/BY-OY/Desktop/新版.html` 的 `page-appointment-calendar`、`modal-appointment-edit`、`renderAppointmentCalendar`、`renderAppointmentDay`、`openAppointmentModal`、`deleteAppointment`,以及本对话三张截图。 + +用户确认:同一目的仓允许多个客户货物统一预约、统一送仓,不能按客户拆预约。客户隔离保留在货物明细。 + +| 页面/操作 | 原型承载与布局 | 实现约束 | +|---|---|---| +| 预约列表 | 左侧月/周日历,右侧选中日期详情 | 保留双栏日历;到港蓝色、送仓绿色,区分预计/实际到港 | +| 新增预约 | 居中双列表单,原型最大宽度760px | 保留居中双列表单;为跨客户货物选择扩至1080px,在同弹框下方分页选择真实入仓货物 | +| 编辑预约 | 复用居中表单 | 与入仓单共用记录;版本校验;真实货物归属不允许手填更改 | +| 到港详情 | 当天安排右侧只读 | 使用货柜/船节点事实,无逐行详情请求,无模拟到港数据 | +| 删除预约 | 二次确认 | 显式提示全部货物解除预约关联;独立删除权限,已关联有效排车拒绝删除 | +| 月/周切换 | 顶部切换与前后按钮 | 周模式按7天移动,修正原型仍按月移动的问题 | +| 多条安排 | 日历标签、右侧明细 | 月/周日格均最多显示3条,超出显示“还有N条”,悬停显示当天总数;日格固定高度,右侧全部展示并独立滚动 | + +预约结束时间未明确时允许为空;不把预计装车时间当结束时间。预约号、目的仓和货物来源遵守服务端合同。日历范围最多42天,每次范围变化查询预约与到港各一次;选择范围内日期不发送请求。候选只在用户点击查询时分页读取,跨页选择保留,重新查询清空。 + +视觉验收目标为1920×1080、100%缩放。该矩阵描述设计和代码边界,不代替测试环境浏览器验收证据。 diff --git a/src/modules/appointment-calendar/api.ts b/src/modules/appointment-calendar/api.ts new file mode 100644 index 00000000..9affcc1f --- /dev/null +++ b/src/modules/appointment-calendar/api.ts @@ -0,0 +1,28 @@ +import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' + +export interface ArrivalCalendarEntry { + containerBookingId: string + bookingNo: string + containerNo?: string | null + vesselName?: string | null + voyageNo?: string | null + destinationPortName?: string | null + destinationWarehouseCode?: string | null + arrivalAt: string + arrivalType: 'ACTUAL' | 'ESTIMATED' +} + +export async function listArrivalCalendar(query: { from: string; to: string }, signal?: AbortSignal) { + const response = await http.get, ApiEnvelope>( + '/platform/v1/port-schedules/arrival-calendar', { params: query, signal }, + ) + return unwrapApiResponse(response) +} + +export async function deleteAppointment(appointmentNo: string, version: number) { + const response = await http.delete, ApiEnvelope>( + `/platform/v1/last-leg/inbound-plans/fba-appointments/${encodeURIComponent(appointmentNo)}`, + { params: { version } }, + ) + return unwrapApiResponse(response) +} diff --git a/src/modules/appointment-calendar/calendar.test.ts b/src/modules/appointment-calendar/calendar.test.ts new file mode 100644 index 00000000..bfd0c581 --- /dev/null +++ b/src/modules/appointment-calendar/calendar.test.ts @@ -0,0 +1,27 @@ +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('moves by a week in week mode instead of following the prototype month-navigation bug', () => { + expect(moveCalendar('2026-09-24', 'week', 1)).toBe('2026-10-01') + expect(calendarDates('2026-09-27', 'week')).toEqual([ + '2026-09-21', '2026-09-22', '2026-09-23', '2026-09-24', + '2026-09-25', '2026-09-26', '2026-09-27', + ]) + }) + + it('clamps month-end selection and handles leap days', () => { + expect(moveCalendar('2028-01-31', 'month', 1)).toBe('2028-02-29') + expect(moveCalendar('2027-03-31', 'month', -1)).toBe('2027-02-28') + expect(addDays('2028-02-28', 1)).toBe('2028-02-29') + expect(() => addDays('2027-02-29', 1)).toThrow('无效的日历日期') + }) +}) diff --git a/src/modules/appointment-calendar/calendar.ts b/src/modules/appointment-calendar/calendar.ts new file mode 100644 index 00000000..c457c48e --- /dev/null +++ b/src/modules/appointment-calendar/calendar.ts @@ -0,0 +1,45 @@ +export type CalendarMode = 'month' | 'week' + +/** Calendar dates are warehouse-local dates, never UTC conversions. */ +export function dateKey(date: Date): string { + return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}` +} + +function parseDate(value: string): Date { + const date = new Date(`${value}T12:00:00`) + if (!/^\d{4}-\d{2}-\d{2}$/.test(value) || Number.isNaN(date.getTime()) || dateKey(date) !== value) { + throw new Error('无效的日历日期') + } + return date +} + +export function addDays(value: string, days: number): string { + const date = parseDate(value) + date.setDate(date.getDate() + days) + return dateKey(date) +} + +export function calendarRange(anchor: string, mode: CalendarMode): { from: string; to: string } { + const date = parseDate(anchor) + 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) } +} + +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)) +} + +export function moveCalendar(anchor: string, mode: CalendarMode, offset: number): string { + if (mode === 'week') return addDays(anchor, offset * 7) + const date = parseDate(anchor) + const day = date.getDate() + date.setDate(1) + date.setMonth(date.getMonth() + offset) + const lastDay = new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate() + date.setDate(Math.min(day, lastDay)) + return dateKey(date) +} diff --git a/src/modules/appointment-calendar/routes.ts b/src/modules/appointment-calendar/routes.ts new file mode 100644 index 00000000..b141884d --- /dev/null +++ b/src/modules/appointment-calendar/routes.ts @@ -0,0 +1,8 @@ +import type { RouteRecordRaw } from 'vue-router' + +export const appointmentCalendarRoutes = [{ + path: 'appointment-calendar', + name: 'appointment-calendar', + component: () => import('./AppointmentCalendarView.vue'), + meta: { title: '预约列表', showPageHeading: false, permission: 'platform:appointment:list' }, +}] satisfies RouteRecordRaw[] diff --git a/src/modules/container-loading/components/ContainerCustomsPanel.test.ts b/src/modules/container-loading/components/ContainerCustomsPanel.test.ts index e6f4e900..6f2c83a2 100644 --- a/src/modules/container-loading/components/ContainerCustomsPanel.test.ts +++ b/src/modules/container-loading/components/ContainerCustomsPanel.test.ts @@ -116,7 +116,8 @@ describe('ContainerCustomsPanel', () => { it('keeps READY_TO_START read-only while exposing source file name and download', async () => { const wrapper = await render('READY_TO_START') expect(wrapper.text()).toContain('开始报关') - expect(wrapper.text()).toContain('实际箱数 12') + expect(wrapper.get('[aria-label="报关概况"] dt').text()).toBe('实际箱数') + expect(wrapper.get('[aria-label="报关概况"] dd').text()).toBe('12') expect(wrapper.text()).toContain('客户退税资料.pdf') expect(wrapper.text()).toContain('客户退税资料') expect(wrapper.text()).toContain('报关行回执') diff --git a/src/modules/container-loading/components/ContainerCustomsPanel.vue b/src/modules/container-loading/components/ContainerCustomsPanel.vue index 041c9db4..65ea7b45 100644 --- a/src/modules/container-loading/components/ContainerCustomsPanel.vue +++ b/src/modules/container-loading/components/ContainerCustomsPanel.vue @@ -291,7 +291,7 @@ onMounted(load)
-

整柜报关

客户退税资料与平台买单事实按柜冻结;报关完成和国内离仓分别展示。

+

整柜报关

资料、回执与完税文件按柜管理。

{{ customs.hasActualCargo ? statusText[customs.status] : '无需报关' }} 导出买单 XLSX @@ -307,8 +307,13 @@ onMounted(load)