diff --git a/AGENTS.md b/AGENTS.md index 2914efa0..b5a9937c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,6 +41,7 @@ | 供应商、国内整柜 | `modules/suppliers/`、`modules/domestic-containers/` | Backend `warehouse/` 及对应 OpenAPI | | 平台仓库与仓储资源 | `modules/warehouses/`、`modules/warehouse-resources/` | `warehouse/` | | 平台用户、RBAC、导航 | `modules/iam/`、`core/access/`、`core/navigation/` | `auth/`、`iam/`、平台权限契约 | +| 海外 PDA 拣货员与实际收货仓绑定 | `modules/iam/pages/InternalUsersView.vue`、`modules/iam/api/platformIam.ts` | `WAREHOUSE_PICKER` 必须且只能绑定一个 `overseasWarehouseId`;候选来自 `modules/warehouses/` 的实际海外仓,服务端约束在 `iam/`,Claim 与实时校验联查 `common-security/`、`lastleg-service/security/` | | 消息中心 | `modules/notifications/` | Backend `iam/.../notification/`;仅顶部消息中心,不进入左侧菜单 | | 订单列表、详情与入库费用 | `modules/unified-orders/`、`modules/first-leg-orders/` | `order/`、`firstleg/`、Finance | | 入库、测量与计价主数据 | `modules/inbounds/`、`modules/fba-pricing/`、`modules/dimension-config/`、`modules/fee-items/` | `firstleg/`、`order/`、`file/`、`integration-service/` | @@ -76,6 +77,7 @@ - 当前只有平台、客户两级;Admin 不显示“租户”。平台仓库是公共主数据,不与客户绑定;客户筛选不得缩小仓库候选。完整语义见平台客户层级与权限契约。 - 菜单、列表路由和详情只读统一使用 `*:list`,写操作使用独立权限;前端守卫不能替代服务端鉴权。供应商固定使用 `platform:supplier:list`。 +- 平台用户选择 `WAREHOUSE_PICKER` 时必须选择唯一实际海外收货仓,其他角色不得提交 `overseasWarehouseId`;编辑时保留已绑定仓库并按字符串透传长整型 ID,前端校验不能替代 IAM 服务端约束。 - `core/navigation/adminNavigation.ts`、`app/router/index.ts` 和模块 `routes.ts` 是共享装配文件;并行任务不得覆盖总控已确认语义,重叠时保留现有改动并回传总控。 - 认证统一经过 `core/auth/auth.ts`、`shared/api/http.ts` 和 `shared/api/overseasHttp.ts`。登录/刷新原子替换国内、海外 Token,海外 401 统一刷新并最多重放一次;页面不读写 Token 或手工拼 `Authorization`。 - 接入真实接口时删除对应模拟路径,不允许生产逻辑静默回退假数据。客户专属写操作显式选择客户,长整型 ID 按字符串透传,不从本地存储推断。 diff --git a/src/app/components/AdminSidebarMenu.test.ts b/src/app/components/AdminSidebarMenu.test.ts index 8b259de6..116661c4 100644 --- a/src/app/components/AdminSidebarMenu.test.ts +++ b/src/app/components/AdminSidebarMenu.test.ts @@ -1,4 +1,6 @@ -import { mount } from '@vue/test-utils' +import { flushPromises, mount } from '@vue/test-utils' +import { defineComponent } from 'vue' +import { createMemoryHistory, createRouter } from 'vue-router' import { describe, expect, it } from 'vitest' import { adminNavigation, filterAdminNavigation, type AdminNavigationNode } from '@/core/navigation/adminNavigation' import AdminSidebarMenu from './AdminSidebarMenu.vue' @@ -19,10 +21,12 @@ const visibleNavigation = filterAdminNavigation(adminNavigation, new Set([ ])) const stubs = { - 'el-menu': { + 'el-menu': defineComponent({ props: ['defaultActive', 'defaultOpeneds', 'collapse'], - template: '', - }, + emits: ['select'], + methods: { updateActiveIndex() {} }, + template: '', + }), 'el-sub-menu': { props: ['index'], template: '
', @@ -34,10 +38,20 @@ const stubs = { } describe('AdminSidebarMenu', () => { + function routerPlugin() { + return createRouter({ + history: createMemoryHistory(), + routes: [ + { path: '/', component: { template: '

' } }, + { path: '/warehouses', component: { template: '

' } }, + ], + }) + } + it('renders the visible phase-one modules and forwards the active route', () => { const wrapper = mount(AdminSidebarMenu, { props: { items: visibleNavigation, activePath: '/system/users', collapsed: false }, - global: { stubs }, + global: { stubs, plugins: [routerPlugin()] }, }) expect(wrapper.get('nav').attributes('data-active')).toBe('/system/users') @@ -62,7 +76,7 @@ describe('AdminSidebarMenu', () => { it('forwards desktop collapse state to Element Plus', () => { const wrapper = mount(AdminSidebarMenu, { props: { items: visibleNavigation, activePath: '/', collapsed: true }, - global: { stubs }, + global: { stubs, plugins: [routerPlugin()] }, }) expect(wrapper.get('nav').attributes('data-collapsed')).toBe('true') @@ -72,7 +86,7 @@ describe('AdminSidebarMenu', () => { const items = filterAdminNavigation(adminNavigation, new Set(['platform:permission:list'])) const wrapper = mount(AdminSidebarMenu, { props: { items, activePath: '/', collapsed: false }, - global: { stubs }, + global: { stubs, plugins: [routerPlugin()] }, }) expect(wrapper.text()).toContain('菜单权限') @@ -100,7 +114,7 @@ describe('AdminSidebarMenu', () => { }] const wrapper = mount(AdminSidebarMenu, { props: { items, activePath: '/tracking', collapsed: false }, - global: { stubs }, + global: { stubs, plugins: [routerPlugin()] }, }) expect(wrapper.findAll('[data-kind="group"]')).toHaveLength(2) @@ -110,7 +124,7 @@ describe('AdminSidebarMenu', () => { it('recreates the menu when asynchronously loaded groups change', async () => { const wrapper = mount(AdminSidebarMenu, { props: { items: [adminNavigation[0]], activePath: '/dashboard', collapsed: false }, - global: { stubs }, + global: { stubs, plugins: [routerPlugin()] }, }) const initialMenu = wrapper.get('nav').element @@ -119,4 +133,17 @@ describe('AdminSidebarMenu', () => { expect(wrapper.get('nav').element).not.toBe(initialMenu) expect(wrapper.text()).toContain('系统管理') }) + + it('navigates explicitly when a leaf menu item is selected', async () => { + const router = routerPlugin() + await router.push('/') + const wrapper = mount(AdminSidebarMenu, { + props: { items: visibleNavigation, activePath: '/', collapsed: false }, + global: { stubs, plugins: [router] }, + }) + + await wrapper.get('nav').trigger('click') + await flushPromises() + expect(router.currentRoute.value.path).toBe('/warehouses') + }) }) diff --git a/src/app/components/AdminSidebarMenu.vue b/src/app/components/AdminSidebarMenu.vue index 7577593e..b34e9ec6 100644 --- a/src/app/components/AdminSidebarMenu.vue +++ b/src/app/components/AdminSidebarMenu.vue @@ -1,5 +1,7 @@