# 前端引入 Pinia 方案(已完成) ## 当前状态 当前方案涉及的核心改造已经完成,现状如下: - `Pinia` 已正式接入项目 - `auth / appearance / menu / notification` 四个 store 已完成落地 - “建议进入 Pinia”的状态范围已全部进入对应 store - 认证、外观、菜单、通知、租户切换上下文均已由 store 统一收口 - 页面侧对认证与外观状态的消费已改为直接读取 store - 原 `utils/auth.ts` 与 `utils/appearance.ts` 兼容层已完成清理 - 前端构建验证已通过 ## 1. 背景 当前前端技术栈为 `Vue 3 + Vue Router + Element Plus + Axios`,尚未引入 `Pinia`。从现有代码看,跨页面共享状态主要通过以下方式维护: - `frontend/src/utils/auth.ts` - 使用 `localStorage` 持久化 token、用户信息、租户信息、权限、界面偏好。 - `frontend/src/utils/appearance.ts` - 使用 `reactive` 单例维护主题和密度状态。 - `frontend/src/views/layout/AppLayout.vue` - 集中维护菜单、租户切换、未读站内信、WebSocket 连接、跨标签页同步等全局状态。 - `window.dispatchEvent` / `window.addEventListener` - 通过 `auth:token-updated`、`in-app-unread-changed`、`storage` 事件做组件间和多 Tab 同步。 该方案在当前阶段可以工作,但随着前端功能继续增加,已经出现以下趋势: - 全局状态来源分散,排查问题时需要同时阅读 `utils`、布局组件和页面组件。 - 业务状态与持久化逻辑、事件广播逻辑耦合较深。 - 租户切换、登录态变更、菜单刷新、通知未读数刷新等链路缺少统一的 store 边界。 - 后续若继续增加全局缓存,会放大“旧租户状态残留”和“页面使用过期缓存”的风险。 结论:建议引入 `Pinia`,但采用渐进式迁移,不进行一次性重构。 ## 2. 目标 本方案目标如下: - 建立统一的前端全局状态管理入口。 - 将认证、租户、菜单、外观、通知等跨页面共享状态从工具函数和布局组件中抽离。 - 降低租户切换、重新登录、刷新 token、多标签页同步等复杂链路的维护成本。 - 为后续增加全局缓存、数据预取、业务上下文复用提供稳定边界。 ## 3. 非目标 本次方案不包含以下内容: - 不将所有页面内 `ref/reactive` 状态迁入 Pinia。 - 不将表单输入、弹窗开关、分页条件等纯页面局部状态全局化。 - 不在首轮引入额外状态管理插件或复杂中间件。 - 不在首轮重构全部页面的接口调用方式。 ## 4. 是否建议引入 建议引入,原因如下: - 项目已经存在明确的全局共享状态。 - 当前共享状态分散在 `localStorage`、响应式单例、布局组件和浏览器事件之间。 - 多租户切换已经是核心链路,继续扩展时需要稳定的 store 清理机制。 - 后续若继续做全局搜索、通知中心、更多个性化设置、页面缓存,Pinia 会显著降低复杂度。 不建议一次性全面迁移,原因如下: - 大部分业务页面仍以单页局部状态为主,强行迁移收益不高。 - 一次性改动风险大,容易引入登录态、权限态和租户态回归问题。 ## 5. 适合进入 Pinia 的状态范围(已完成) 已完成进入 Pinia: - 认证态 - token - 当前用户 - 当前租户 - scope - roles - permissions - 外观态 - themeMode - density - 初始化状态 - 菜单与布局态 - 当前菜单列表 - 当前租户 logo - 是否正在切换租户 - 通知态 - 未读数 - 通知列表 - 轮询和 WebSocket 生命周期状态 - 租户切换上下文 - 当前租户 ID - 切换后是否需要清理的业务缓存版本号 不建议进入 Pinia: - 单页面查询条件 - 表单内容 - 弹窗显隐 - 临时上传进度 - 仅在单个页面使用的表格数据 ## 6. 推荐目录结构 建议在 `frontend/src` 下新增如下目录: ```text stores/ index.ts auth.ts appearance.ts menu.ts notification.ts ``` 职责建议如下: - `stores/index.ts` - 统一创建 `pinia` 实例。 - `stores/auth.ts` - 管理登录态、用户信息、权限、租户信息、登录态广播、登出清理。 - `stores/appearance.ts` - 管理主题、密度、文档样式同步。 - `stores/menu.ts` - 管理租户端和平台端菜单加载、菜单刷新。 - `stores/notification.ts` - 管理未读数、通知列表、轮询、WebSocket 连接和重连。 ## 7. 设计原则 ### 7.1 store 负责状态和动作,工具函数只保留纯函数能力 例如: - `utils/auth.ts` - 逐步收缩为解析、序列化、兼容读取等纯工具。 - 登录保存、登出清理、权限变更广播 - 迁移到 `authStore`。 ### 7.2 持久化与内存态分层 - `Pinia` 中维护运行时状态。 - `localStorage` 仅作为持久化载体,不再作为主要读取入口。 - 页面和组件默认从 store 读取,不直接散落读取 `localStorage`。 ### 7.3 租户切换必须具备显式清理机制 租户切换后,至少要做到: - 刷新 `authStore` - 刷新 `menuStore` - 刷新 `notificationStore` - 清空或失效租户相关业务缓存 - 触发页面重新拉数或重建 建议在 `authStore` 中增加: - `authVersion` - `tenantContextVersion` 通过版本号驱动需要重新加载的数据,而不是只依赖浏览器事件和页面手工处理。 ### 7.4 不引入“超大 store” 避免把全部全局状态堆进一个 `appStore`。按领域拆分,降低耦合。 ## 8. 分阶段实施方案 ### 阶段 1:基础接入(已完成) 目标: - 引入 `pinia` - 完成基础目录和初始化接入 - 不改变现有业务行为 实施内容: - 安装 `pinia` - 新增 `frontend/src/stores/index.ts` - 在 `frontend/src/main.ts` 中接入 `app.use(pinia)` - 新增空的 `authStore`、`appearanceStore`、`menuStore`、`notificationStore` 产出: - 项目具备 store 基础设施 - 不触发业务逻辑迁移 ### 阶段 2:迁移认证与外观(已完成) 目标: - 优先迁移最稳定、最全局的状态 实施内容: - 将 `utils/auth.ts` 中的状态读写入口迁移到 `authStore` - 将 `utils/appearance.ts` 中的响应式单例迁移到 `appearanceStore` - 保留 `localStorage` 持久化,但统一由 store 写入 - 页面不再直接读取 `localStorage` 获取认证信息 涉及文件重点: - `frontend/src/utils/auth.ts` - `frontend/src/utils/appearance.ts` - `frontend/src/main.ts` - `frontend/src/App.vue` - `frontend/src/router/index.ts` - `frontend/src/views/modules/ProfilePage.vue` 验收标准: - 登录、刷新、退出登录功能不回归 - 主题和密度切换行为不回归 - 多 Tab 基本同步仍然可用 ### 阶段 3:迁移菜单与租户切换链路(已完成) 目标: - 将当前集中在 `AppLayout.vue` 的布局级共享状态抽出 实施内容: - 将菜单加载迁移到 `menuStore` - 将租户切换迁移到 `authStore` 或专门的 `tenant context` action - 将租户 Logo、租户切换状态从布局组件迁移到 store - 明确切租户后的 store 级缓存清理动作 涉及文件重点: - `frontend/src/views/layout/AppLayout.vue` - `frontend/src/api/modules.ts` - `frontend/src/router/index.ts` 验收标准: - 租户切换后菜单正确刷新 - 原路由保留逻辑仍然成立 - 不出现旧租户菜单、旧租户 logo、旧租户权限残留 ### 阶段 4:迁移通知中心与 WebSocket 生命周期(已完成) 目标: - 将布局中的通知状态与连接管理从视图层抽离 实施内容: - 将未读数、通知列表迁移到 `notificationStore` - 将轮询、WebSocket 建连、重连、关闭逻辑迁移到 store action - 保留 `AppLayout.vue` 作为纯展示和交互入口 验收标准: - 未读数更新正常 - 标记已读/全部已读正常 - 页面切换不导致重复连接 - 登出或切租户后连接被正确关闭 ## 9. 与现有代码的映射关系(已完成) 建议按如下方式收口: - 当前 `frontend/src/utils/auth.ts` - 收口到 `authStore` - 当前 `frontend/src/utils/appearance.ts` - 收口到 `appearanceStore` - 当前 `frontend/src/views/layout/AppLayout.vue` - 收口到 `menuStore + notificationStore + authStore` - 当前直接调用 `hasPermission()` 的页面 - 后续逐步改为从 `authStore` 暴露的 getter 或 helper 读取 ## 10. 持久化策略建议 首轮建议不引入额外持久化插件,采用手工持久化,原因如下: - 当前状态字段较少,手工维护可控。 - 可以保留现有 `localStorage` 键,降低兼容成本。 - 可以在迁移期同时兼容旧逻辑,便于灰度替换。 后续如 store 数量和持久化字段继续增多,再评估是否引入持久化插件。 ## 11. 风险与应对 ### 风险 1:登录态回归 表现: - 登录后页面未刷新权限 - token 刷新后页面仍使用旧状态 应对: - 认证迁移阶段保留兼容读取逻辑 - 为登录、登出、refreshAuth、tenant switch 增加回归测试清单 ### 风险 2:租户切换后残留旧缓存 表现: - 菜单、通知、页面数据、权限按钮残留旧租户信息 应对: - 引入 `tenantContextVersion` - 在切换租户 action 中显式调用各 store 的 `reset` 或 `reload` ### 风险 3:组件和 store 职责混乱 表现: - 视图层继续直接读写 `localStorage` - store 同时处理过多页面局部逻辑 应对: - 代码评审时明确约束 - 新增共享状态优先进入 store - 页面局部状态继续留在页面组件 ## 12. 预估改造顺序 推荐顺序: 1. `pinia` 基础接入 2. `appearanceStore` 3. `authStore` 4. `menuStore` 5. `notificationStore` 6. 页面侧逐步替换直接读取 `localStorage` 的逻辑 这样做的原因: - `appearance` 和 `auth` 边界最清晰,迁移成本最低。 - `menu` 和 `notification` 与租户切换耦合更深,适合第二阶段处理。 - 页面层替换可以跟随 store 稳定后分批推进。 ## 13. 验收结果(已完成) 完成首轮引入后,应满足以下标准: - 项目已正式接入 `Pinia` - 认证态、外观态不再以分散的 `localStorage` 读取为主入口 - 租户切换存在明确的 store 级清理机制 - 菜单和通知逻辑从 `AppLayout.vue` 中部分或全部抽离 - 现有登录、登出、刷新 token、切换租户、多 Tab 同步能力不回退 ## 14. 最终状态(已完成) 建议采用“引入基础设施 + 迁移最核心全局状态 + 保留页面局部状态”的方式推进。 具体建议如下: - 建议引入 `Pinia` - 不建议一次性重构所有页面 - 第一批只迁移 `auth / appearance / menu / notification` - 明确把“租户切换后的 store 清理机制”作为本次方案的硬性要求 该方案执行后,前端状态管理会从“工具函数 + 布局组件 + 浏览器事件”的分散模式,过渡到“store 统一收口、页面按需消费”的结构,更适合当前项目继续演进。