--- name: migrate-default-layout overview: 将 fms-vue-old2 的 DefaultLayout 布局体系(AppSidebar/AppTopbar/AppTabs/NavMain/NavMenuItem)整体迁移到 fms-vue,tailwind 改为 scoped scss + --fms-* token,不引入新依赖,动态菜单/页签/主题/权限逻辑与 old2 保持一致。 todos: - id: migrate-utils-stores content: 迁移 utils/tree.js、utils/lucideIcons.js 与 stores/permissions.js,扩展 stores/app.js(页签/折叠/语言/刷新状态) status: completed - id: layout-shell content: 创建 DefaultLayout.vue 与 scss 布局骨架:拖拽调宽、keep-alive、页面过渡,手写事件替代 @vueuse/core status: completed dependencies: - migrate-utils-stores - id: sidebar-menu content: 自研 NavMain/NavMenuItem 递归菜单与 AppSidebar(团队切换 + 动态权限菜单),参考 [skill:ui-ux-pro-max] 视觉规则 status: completed dependencies: - layout-shell - id: topbar-tabs content: 实现 AppTopbar(面包屑/主题/语言/用户下拉,复用 ThemeToggle/ColorThemePicker/Dropdown/Message)与 AppTabs 页签栏 status: completed dependencies: - layout-shell - id: router-dashboard content: 改造路由挂 DefaultLayout 承载子路由并加权限守卫,调整 dashboard 为布局内内容页 status: completed dependencies: - sidebar-menu - topbar-tabs --- ## 产品概述 将 fms-vue-old2 的整个布局体系(DefaultLayout 及其 menu/topbar/tabs/content/navmain)迁移到 fms-vue 业务项目,样式从 tailwindcss 改写为自研 scss(基于 --fms-* token),不需要响应式。菜单组件自研并放置于 src/layouts/components(不进 components/ui)。同步迁移权限 store、树/图标工具,扩展 app store 以支持页签、路由刷新、侧栏折叠、语言状态,路由改造为布局壳承载子路由。 ## 核心功能 - 布局壳 DefaultLayout:左侧可拖拽调宽侧边栏(180-400px)+ 右侧内容区(topbar + 页签栏 + router-view),页面过渡动画 + keep-alive 缓存 - AppSidebar:顶部团队切换 + 动态菜单(基于权限模块数据构建树,仅保留有权限的菜单项),可折叠为图标栏 - AppTopbar:侧栏折叠按钮、面包屑(路由 meta.title/section)、语言切换、主题色板与深浅色切换(复用 ThemeToggle/ColorThemePicker)、通知、用户下拉(头像/退出登录,退出用 Message 提示) - AppTabs:多页签栏(跟随访问路由自动添加、可关闭、溢出滚动、双击刷新),dashboard 固定不可关 - 权限体系:permissions store(loadDataApi 拉取模块/用户权限数据,g3soft 提权,canAccess/canPower),路由守卫在 moduleId 存在时做权限拦截 - 路由改造:/login 独立;/ 父路由挂 DefaultLayout(redirect /dashboard),dashboard 为子路由;/demo 保持独立不进布局;catchAll 不变 ## 边界 - 不引入任何新 npm 依赖:@vueuse/core(useEventListener/usePreferredReducedMotion/useResizeObserver)全部手写;vue-sonner toast 用自研 Message 替代;shadcn sidebar/dropdown-menu/avatar/breadcrumb/separator/collapsible 一律不迁移 - 无响应式:移除所有断点类,布局固定桌面形态 - 主题沿用 fms-vue 现有体系(--fms-primary + .dark),不迁移 old2 的 oklch scheme 面板,色板复用 ColorThemePicker ## 技术栈 - Vue 3.5 + Vite 8(现有),纯 JavaScript,自研 scss 样式(sass 已安装) - 现有依赖复用:@lucide/vue(图标)、pinia + pinia-plugin-persistedstate、vue-router、nprogress、自研组件库(button/dropdown/message/popover/tooltip/spin) - 不新增依赖;@vueuse/core / vue-sonner / shadcn-vue 相关代码以手写或现有组件替代 ## 实现方案 分层架构与 old2 一致:layouts(布局壳)→ stores(app/permissions/auth)→ services(http/api)→ utils(tree/lucideIcons)。 关键决策: 1. **组件替换映射**(tailwind/shadcn → 自研): - SidebarProvider/SidebarInset/SidebarTrigger → 自研 flex scss 布局 + 折叠状态存 app store(sidebarCollapsed),侧栏宽 240px(展开)/ 64px(折叠) - DropdownMenu(topbar 用户菜单/色板、sidebar 团队切换)→ fms-vue Dropdown(items 数组或 #menu 插槽),icon/shortcut/divider 均已支持 - Avatar → 自研圆形首字母头像(CSS) - Breadcrumb/Separator → 自研面包屑(flex + span)与 CSS 分隔线 - toast → Message.info/success/error(已有命令式 API) - Collapsible(菜单展开)→ NavMenuItem 自研递归组件 + 手写展开状态 2. **手写 composables**(替代 @vueuse/core):onMounted/onBeforeUnmount 注册移除 window 事件(mousemove/mouseup 拖拽调宽)、matchMedia('(prefers-reduced-motion: reduce)')、ResizeObserver(页签溢出检测);组件卸载时全部清理,避免内存泄漏 3. **菜单自研**:NavMain 渲染 NavMenuItem 递归(depth 递归):有 children 的目录项 → 可展开/收起(默认展开含激活子项的父级,hasActiveChild 判断);叶子项 → router-link + isActive 高亮(route.path === url 或 startsWith url + '/');折叠态下目录项点击展开、叶子仅图标 + tooltip 悬浮提示 4. **app store 扩展**(保持现有 options API 兼容):新增 sidebarCollapsed、visitedTabs(初始含 dashboard)、routeRefreshKeys + addVisitedTab/removeVisitedTab/getRouteRefreshKey/refreshRoute/toggleSidebar;现有 darkMode/primaryColor/initThemeSync 不变(ThemeToggle/ColorThemePicker 依赖它们) 5. **权限 store 迁移**:JS 版逻辑与 old2 完全一致(load 去重加载、g3soft 提权、resolvePageId/canAccess/canPower、watch auth 变化 clear),依赖已有 loadDataApi 6. **路由改造**:/ 父路由 `component: DefaultLayout, redirect: '/dashboard', meta: { requiresAuth: true }`,子路由 /dashboard(title 工作台,id 固定);beforeEach 保留登录守卫 + 新增 moduleId 权限校验(await permissionStore.load() 后 canAccess 失败跳 /dashboard);/demo 独立路由不进布局 7. **dashboard 页面**:移除原独立全屏占位样式,改为 layout 内容区内的简单内容页(标题 + 建设中提示,删除退出登录按钮——退出已由 topbar 负责) 8. **性能与可靠性**:keep-alive max 8 + routeInstanceKey(fullPath:refreshKey)保持;拖拽调宽用 rAF 节流避免频繁重排;页签滚动按钮按需渲染(scrollWidth > clientWidth 判定);所有手写监听在 onBeforeUnmount 清理 ## 实施要点 - DefaultLayout 的页面过渡 CSS(.page-enter-active 等)写在非 scoped style,并包裹 @media (prefers-reduced-motion: reduce) 禁用 - 拖拽 rail 交互:mousedown 记录 startX/startWidth,mousemove 计算 180-400px 夹逼,直接写 inline width 到侧栏根元素;mouseup 移除监听 - AppTabs 的 wheel 横向滚动需 e.preventDefault()(非 passive 监听),overflow 检测用 ResizeObserver + scroll 事件 - topbar 语言切换仅切换 store.locale 状态(项目暂无 i18n 文案,按钮保留占位),通知按钮保留占位 - 面包屑从 route.meta.title/section 派生;dashboard 无 section 时显示单级"工作台" - 用户下拉:Dropdown #menu 插槽自定义内容(用户名/orgId/退出登录),退出调 authStore.clearSession + Message.info + router.replace('/login') - 删除项:old2 的 oklch scheme 面板、shadcn sidebar 相关选择器(data-slot、group-data 等)不迁移;@vueuse/core/vue-sonner import 全部移除 - 不主动执行构建/测试;交付后 lint 检查 + 说明手动验证路径(pnpm dev → 登录 → dashboard 布局/页签/菜单/退出) ## 架构与数据流 ```mermaid flowchart LR A[访问 /] --> B{router.beforeEach} B -->|未登录| C[/login/] B -->|已登录| D[DefaultLayout] D --> E[AppSidebar] D --> F[AppTopbar] D --> G[AppTabs] D --> H[router-view + keep-alive] E --> I[permissions store] I --> J[loadDataApi 拉取模块] E --> K[NavMain/NavMenuItem 递归菜单] F --> L[退出登录 clearSession] G --> M[app store visitedTabs] H --> N[子路由 dashboard 等] ``` ## 目录结构(变更清单) ``` fms-vue/ ├── src/ │ ├── layouts/ │ │ ├── DefaultLayout.vue # [NEW] 布局壳:侧栏 + 内容区 + keep-alive/过渡 + 拖拽调宽 │ │ ├── index.scss # [NEW] 布局壳 scss(非 scoped 全局布局样式 + 页面过渡) │ │ └── components/ │ │ ├── AppSidebar.vue # [NEW] 团队切换 + 动态菜单容器(权限数据构建树) │ │ ├── AppTopbar.vue # [NEW] 折叠/面包屑/语言/主题/通知/用户下拉 │ │ ├── AppTabs.vue # [NEW] 页签栏(添加/关闭/滚动/双击刷新) │ │ ├── NavMain.vue # [NEW] 菜单列表容器 │ │ ├── NavMenuItem.vue # [NEW] 递归菜单项(展开/折叠/高亮/图标) │ │ └── index.scss # [NEW] 侧栏/菜单/topbar/页签 scss(--fms-* token) │ ├── stores/ │ │ ├── app.js # [MODIFY] 新增 sidebarCollapsed/visitedTabs/routeRefreshKeys/locale + actions │ │ ├── permissions.js # [NEW] 权限 store(load/canAccess/canPower/resolvePageId/clear) │ │ └── auth.js # 不变 │ ├── utils/ │ │ ├── tree.js # [NEW] buildTree(b_id/b_parent_id)/sortTree(b_xh,b_id) │ │ └── lucideIcons.js # [NEW] getIcon(name) 按名取 @lucide/vue 图标 │ ├── router/ │ │ └── index.js # [MODIFY] / 挂 DefaultLayout + dashboard 子路由 + 权限守卫 │ ├── views/ │ │ ├── dashboard/index.vue # [MODIFY] 改为布局内内容页(去掉独立全屏壳/退出按钮) │ │ ├── login/index.vue # 不变 │ │ └── demo/index.vue # 不变 │ ├── App.vue # 不变(router-view) │ └── main.js # 不变(initThemeSync 保持) └── package.json # 不变(不新增依赖) ``` ## 关键约定 - 所有组件 props/事件命名沿用自研组件库现有 API(如 Button type/block/loading、Dropdown items/#menu(close)) - 页面过渡与全局布局样式放 DefaultLayout 非 scoped style;组件内布局细节用 scoped scss - 不使用 TS 类型标注;复杂接口用 JSDoc 注释说明(如 NavItem:{ title, url, icon?, items?, moduleId? }) ## Agent Extensions ### Skill - **ui-ux-pro-max** - 用途:查询企业后台管理布局(侧边栏导航/顶栏/页签)的视觉与交互规则,指导自研菜单与 topbar 的 scss 样式细节(hover/active 态、间距、层级),确保迁移后视觉符合现代企业 SaaS 标准 - 预期结果:自研 NavMain/NavMenuItem/AppTopbar/AppTabs 的 scss 达到与 old2 视觉质量相当的水平(若脚本无法执行则读取其 references 规则文档落地)