Files
workspace/code/fms/.codebuddy/plans/migrate-default-layout_5046b992.md
T
2026-08-16 22:01:32 +08:00

154 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 规则文档落地)