diff --git a/AGENTS.md b/AGENTS.md index 414358d1..3fad531f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,13 +2,14 @@ ## 快速读取顺序 -1. 用“模块快速路由”选择最小业务模块,进入后读取该目录最近的 `AGENTS.md`;按 `routes.ts → pages/ → api/types/stores/composables` 下钻,不先扫描全部 `src/`。 -2. 视觉还原、布局/交互承载方式变更或真实页面验收时读取 `../../docs/Admin-Client前端原型还原验收规则.md`;需要原型依据才读取 `../../docs/悦/货代SAAS平台后台/AGENTS.md` 并定位目标片段。纯文案、逻辑或单元测试任务不自动读取。 - Admin 样式优化还须读取 `../../docs/Admin高密度界面规范.md` 的“样式维护边界”和“新模块接入规则”;以已确认密度与彩色页签为视觉基线,逐操作保留业务语义和承载方式。国内仓、客户管理是样板;新增菜单按本轮范围显式接入,货柜管理入口见 `../../docs/Admin货柜管理样式核对记录.md`。 -3. 标准列表或公共组件任务读取 `src/shared/components/AGENTS.md`;国内/海外 HTTP、Token 或刷新任务读取 `src/shared/api/AGENTS.md`;Finance 任务读取 `src/modules/finance/AGENTS.md`;海外仓全流程任务先读 `../../docs/海外仓业务/README.md`。 -4. 平台/客户层级与权限公共语义读取 `../../docs/平台客户层级与权限契约.md`;统一 API、Token、会话撤销或可靠事件任务按需读取 `../../docs/统一认证与服务通信契约.md` 对应章节。 +1. 由“模块快速路由”选最小模块并读最近 `AGENTS.md`,按 `routes.ts → pages/ → api/types/stores/composables` 下钻,不扫描全部 `src/`。 +2. 视觉、布局/交互承载变更或真实页面验收先读 `../../docs/Admin-Client前端原型还原验收规则.md`;需原型依据再读 `../../docs/悦/货代SAAS平台后台/AGENTS.md` 并定位片段。纯文案、逻辑或单元测试不触发。 + 样式优化另读 `../../docs/Admin高密度界面规范.md` 的“样式维护边界”“新模块接入规则”,保留已确认密度、彩色页签、业务语义及承载。国内仓、客户管理为样板;新增菜单按本轮范围显式接入,货柜入口见 `../../docs/Admin货柜管理样式核对记录.md`。 + 蓝白样式接入见 [样板指南](../../docs/界面参考/蓝白卡片式工作台风格参考.md)。 +3. 标准列表/公共组件读 `src/shared/components/AGENTS.md`;HTTP、Token/刷新读 `src/shared/api/AGENTS.md`;Finance 读 `src/modules/finance/AGENTS.md`;海外仓全流程先读 `../../docs/海外仓业务/README.md`。 +4. 平台/客户层级及权限读 `../../docs/平台客户层级与权限契约.md`;统一 API、Token、会话撤销或可靠事件按需读 `../../docs/统一认证与服务通信契约.md` 对应章节。 -普通单端任务的最小读取集是本文件、目标模块最近的 `AGENTS.md` 和目标代码命中段;只有跨服务状态、权限或接口边界无法判断时才增加后端契约。读取预算遵循根指引的行数与字符数标准,先读适用规则,再定向读取业务表,不全文加载无关专题。 +单端仅读本指引、最近模块指引及命中代码;跨服务状态、权限或接口边界不明才加后端契约。预算遵循根指引,先读适用规则、定向查业务表,跳过无关专题。 ## 项目定位与技术基线 diff --git a/src/app/components/AdminSidebarMenu.test.ts b/src/app/components/AdminSidebarMenu.test.ts index ba96e2ff..e2e7e696 100644 --- a/src/app/components/AdminSidebarMenu.test.ts +++ b/src/app/components/AdminSidebarMenu.test.ts @@ -1,7 +1,10 @@ import { flushPromises, mount } from '@vue/test-utils' -import { defineComponent } from 'vue' +import { defineComponent, markRaw } from 'vue' +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' import { createMemoryHistory, createRouter } from 'vue-router' -import { describe, expect, it } from 'vitest' +import { ElMenu, ElMenuItem, ElSubMenu } from 'element-plus' +import { describe, expect, it, vi } from 'vitest' import { adminNavigation, filterAdminNavigation, type AdminNavigationNode } from '@/core/navigation/adminNavigation' import AdminSidebarMenu from './AdminSidebarMenu.vue' @@ -85,6 +88,72 @@ describe('AdminSidebarMenu', () => { expect(wrapper.get('nav').attributes('data-collapsed')).toBe('true') }) + it('fits collapsed icons inside the scrolling rail and keeps hover submenus outside it', async () => { + const host = document.createElement('div') + host.className = 'app-shell is-collapsed' + host.innerHTML = '' + document.body.append(host) + const style = document.createElement('style') + style.textContent = readFileSync(resolve(process.cwd(), 'src/styles/shell.css'), 'utf8') + document.head.append(style) + const rail = host.querySelector('.side-nav')! + const router = routerPlugin() + await router.push('/') + const icon = markRaw(adminNavigation[0].icon) + const items: AdminNavigationNode[] = [ + { key: 'home', title: '首页', icon, path: '/' }, + { + key: 'resources', title: '资源管理', icon, + children: [{ key: 'warehouses', title: '仓库管理', icon, path: '/warehouses' }], + }, + ] + const wrapper = mount(AdminSidebarMenu, { + attachTo: rail, + props: { items, activePath: '/', collapsed: true }, + global: { plugins: [router], components: { ElMenu, ElMenuItem, ElSubMenu }, stubs: { transition: false } }, + }) + try { + await flushPromises() + const menu = wrapper.get('.side-nav-menu.el-menu--collapse') + const group = wrapper.get('.side-nav-menu > .el-sub-menu') + const leafTrigger = wrapper.get('.el-menu-tooltip__trigger') + expect(getComputedStyle(host.querySelector('.app-sidebar')!).width).toBe('76px') + expect(getComputedStyle(rail).overflowX).toBe('hidden') + expect(getComputedStyle(rail).overflowY).toBe('auto') + expect(getComputedStyle(menu.element).width).toBe('100%') + for (const trigger of [leafTrigger, group.get('.el-sub-menu__title')]) { + expect(getComputedStyle(trigger.element).paddingLeft).toBe('0px') + expect(getComputedStyle(trigger.element).paddingRight).toBe('0px') + expect(getComputedStyle(trigger.element).justifyContent).toBe('center') + expect(getComputedStyle(trigger.get('.menu-icon').element).marginRight).toBe('0px') + } + + await group.trigger('mouseenter') + await vi.waitFor(() => expect(group.attributes('aria-expanded')).toBe('true')) + const popup = document.body.querySelector('.shell-nav-popper .el-menu--popup') + expect(popup).not.toBeNull() + expect(popup?.closest('.side-nav')).toBeNull() + expect(popup?.textContent).toContain('仓库管理') + + const destination = popup!.querySelector('.el-menu-item')! + destination.focus() + expect(document.activeElement).toBe(destination) + expect(destination.getAttribute('role')).toBe('menuitem') + destination.click() + await flushPromises() + expect(router.currentRoute.value.path).toBe('/warehouses') + + await wrapper.setProps({ collapsed: false }) + host.classList.remove('is-collapsed') + expect(getComputedStyle(host.querySelector('.app-sidebar')!).width).toBe('240px') + expect(wrapper.find('.el-menu--collapse').exists()).toBe(false) + } finally { + wrapper.unmount() + style.remove() + host.remove() + } + }) + it('renders only entries included in the filtered navigation input', () => { const items = filterAdminNavigation(adminNavigation, new Set(['platform:permission:list'])) const wrapper = mount(AdminSidebarMenu, { diff --git a/src/app/components/AdminSidebarMenuNode.vue b/src/app/components/AdminSidebarMenuNode.vue index ab50c0da..c7e6f07c 100644 --- a/src/app/components/AdminSidebarMenuNode.vue +++ b/src/app/components/AdminSidebarMenuNode.vue @@ -10,7 +10,7 @@ defineProps<{