# 状态管理 **本文引用的文件** - [auth.js](file://fms-vue/src/stores/auth.js) - [app.js](file://fms-vue/src/stores/app.js) - [permissions.js](file://fms-vue/src/stores/permissions.js) - [main.js](file://fms-vue/src/main.js) - [router/index.js](file://fms-vue/src/router/index.js) - [api.js](file://fms-vue/src/services/api.js) - [theme/index.js](file://fms-vue/src/theme/index.js) - [stores.spec.js](file://fms-vue/tests/stores/stores.spec.js) - [permissions.spec.js](file://fms-vue/tests/permissions.spec.js) ## 目录 1. [简介](#简介) 2. [项目结构](#项目结构) 3. [核心组件](#核心组件) 4. [架构总览](#架构总览) 5. [详细组件分析](#详细组件分析) 6. [依赖关系分析](#依赖关系分析) 7. [性能考虑](#性能考虑) 8. [故障排查指南](#故障排查指南) 9. [结论](#结论) 10. [附录](#附录) ## 简介 本文件为 FMS 前端(Vue + Pinia)的状态管理文档,聚焦以下目标: - 基于 Pinia 的 store 创建、模块化与数据持久化策略 - 用户认证状态:登录态、用户信息、JWT 令牌处理 - 应用全局状态:主题、语言偏好、UI 状态(侧边栏、页签等) - 权限状态:菜单权限、按钮/功能点权限控制逻辑 - 状态更新最佳实践与性能优化 - 状态调试与测试方法 ## 项目结构 FMS 前端采用按领域划分的 store 模块: - stores/auth.js:认证与会话、用户回显信息 - stores/app.js:应用级 UI 状态(主题、语言、页签、路由刷新计数) - stores/permissions.js:权限数据加载与访问/功能点判断 - router/index.js:路由守卫统一触发权限加载与鉴权 - services/api.js:后端接口封装(登录、数据加载等) - theme/index.js:主题切换与主色变量写入 DOM - main.js:Pinia 初始化、插件启用、主题同步初始化 ```mermaid graph TB A["main.js
创建 Pinia、启用持久化插件"] --> B["stores/app.js
应用状态与主题同步"] A --> C["stores/auth.js
认证与会话持久化"] A --> D["stores/permissions.js
权限数据与判断"] E["router/index.js
beforeEach 守卫"] --> C E --> D D --> F["services/api.js
loadDataApi 等"] B --> G["theme/index.js
applyDarkMode / applyPrimaryColor"] ``` 图表来源 - [main.js:14-22](file://fms-vue/src/main.js#L14-L22) - [app.js:6-102](file://fms-vue/src/stores/app.js#L6-L102) - [auth.js:4-48](file://fms-vue/src/stores/auth.js#L4-L48) - [permissions.js:13-195](file://fms-vue/src/stores/permissions.js#L13-L195) - [router/index.js:109-133](file://fms-vue/src/router/index.js#L109-L133) - [api.js:16-24](file://fms-vue/src/services/api.js#L16-L24) - [theme/index.js:6-15](file://fms-vue/src/theme/index.js#L6-L15) 章节来源 - [main.js:14-22](file://fms-vue/src/main.js#L14-L22) - [router/index.js:109-133](file://fms-vue/src/router/index.js#L109-L133) ## 核心组件 - 认证 Store(auth) - 维护会话凭证(token、user)与跨会话用户回显信息(orgId、account) - 通过 pinia-plugin-persistedstate 将会话写 sessionStorage,用户回显写 localStorage - 提供 setSession、clearSession、saveLastLogin 等方法 - 应用 Store(app) - 管理暗色模式、主色、侧边栏折叠、语言、已访问页签、路由刷新计数 - 通过 watch 将状态同步到 DOM(主题类名与 CSS 变量) - 仅持久化主题与语言相关字段 - 权限 Store(permissions) - 拉取模块、用户授权、功能点数据,并缓存结果 - 支持管理员提权(g3soft)直接可见全部启用模块 - 提供 canAccess(菜单/页面访问)、canPower(功能点/按钮)判断 - 监听认证主体变化自动清理缓存,避免脏数据 章节来源 - [auth.js:4-48](file://fms-vue/src/stores/auth.js#L4-L48) - [app.js:6-123](file://fms-vue/src/stores/app.js#L6-L123) - [permissions.js:13-195](file://fms-vue/src/stores/permissions.js#L13-L195) ## 架构总览 下图展示了从路由守卫到权限加载、再到视图渲染的整体流程。 ```mermaid sequenceDiagram participant U as "用户" participant R as "router/index.js" participant A as "stores/auth.js" participant P as "stores/permissions.js" participant S as "services/api.js" participant V as "视图组件" U->>R : 访问受保护路由 R->>A : 检查 isAuthenticated alt 未登录 R-->>U : 重定向到 /login?redirect=... else 已登录 R->>P : await load() P->>S : loadDataApi(...) 多次并发 S-->>P : 返回模块/授权/功能点数据 P-->>R : 完成加载 R->>V : 允许进入并渲染 end ``` 图表来源 - [router/index.js:109-133](file://fms-vue/src/router/index.js#L109-L133) - [permissions.js:41-109](file://fms-vue/src/stores/permissions.js#L41-L109) - [api.js:16-24](file://fms-vue/src/services/api.js#L16-L24) ## 详细组件分析 ### 认证 Store(auth.js) - 状态设计 - loginInfo:包含 token 与 user,用于当前会话鉴权 - userInfo:包含 orgId 与 account,用于登录页回显,退出登录不清除 - isAuthenticated:计算属性,基于 token 是否存在 - 关键方法 - setSession:兼容多种字段形态(token/access_token、user/user_info),设置 orgId - clearSession:清空会话凭证,保留 userInfo - saveLastLogin:保存机构与账号,供下次打开登录页回显 - 持久化策略 - 使用 pinia-plugin-persistedstate - 会话凭证(loginInfo)持久化到 sessionStorage,键名为 fms-login - 用户回显(userInfo)持久化到 localStorage,键名为 fms-user - 安全与体验 - 会话随标签页关闭失效;回显数据跨会话保留,提升用户体验 ```mermaid flowchart TD Start(["调用 setSession"]) --> Normalize["规范化字段
token/access_token, user/user_info"] Normalize --> SetState["写入 loginInfo 与 userInfo.orgId"] SetState --> PersistSS["持久化到 sessionStorage(fms-login)"] SetState --> PersistLS["持久化到 localStorage(fms-user)"] PersistSS --> End(["完成"]) PersistLS --> End ``` 图表来源 - [auth.js:14-29](file://fms-vue/src/stores/auth.js#L14-L29) - [auth.js:41-46](file://fms-vue/src/stores/auth.js#L41-L46) 章节来源 - [auth.js:4-48](file://fms-vue/src/stores/auth.js#L4-L48) - [stores.spec.js:134-200](file://fms-vue/tests/stores/stores.spec.js#L134-L200) ### 应用 Store(app.js) - 状态设计 - darkMode、primaryColor:主题相关 - sidebarCollapsed:侧边栏折叠状态 - locale:语言偏好(占位) - visitedTabs:已访问页签列表(工作台固定不可关闭) - routeRefreshKeys:路由刷新计数器,key 为 fullPath - 计算属性 - cachedViews:根据 visitedTabs 生成 keep-alive 的 include 列表(去重) - 动作 - 主题切换:setDark/toggleDark/setPrimary - 侧边栏:toggleSidebar/setSidebar - 语言:toggleLocale - 页签管理:addVisitedTab/removeVisitedTab/closeOthers/closeLeft/closeRight/closeAll - 路由刷新:refreshRoute/getRouteRefreshKey/pruneOrphanRefreshKeys - 持久化 - 仅持久化 darkMode、primaryColor、locale - 主题同步 - initThemeSync:在 Pinia 实例激活后建立 watch,将状态同步到 DOM(class 与 CSS 变量) ```mermaid classDiagram class AppStore { +boolean darkMode +string primaryColor +boolean sidebarCollapsed +string locale +Object[] visitedTabs +Object~number~ routeRefreshKeys +cachedViews() string[] +setDark(value) void +toggleDark() void +setPrimary(color) void +toggleSidebar() void +setSidebar(collapsed) void +toggleLocale() void +addVisitedTab(tab) void +removeVisitedTab(key) void +closeOthers(key) void +closeLeft(key) void +closeRight(key) void +closeAll() void +pruneOrphanRefreshKeys() void +getRouteRefreshKey(key) number +refreshRoute(key) void } ``` 图表来源 - [app.js:6-102](file://fms-vue/src/stores/app.js#L6-L102) 章节来源 - [app.js:6-123](file://fms-vue/src/stores/app.js#L6-L123) - [stores.spec.js:19-132](file://fms-vue/tests/stores/stores.spec.js#L19-L132) ### 权限 Store(permissions.js) - 数据模型 - modules:可用模块列表 - grantedPageIds:用户授权的页面 ID 集合 - moduleCodes:用户可访问的模块编码集合 - powerCodes:用户可用的功能点编码集合(形如 moduleId.powerCode) - isG3soft:是否为管理员提权账号 - 加载逻辑 - 管理员提权:仅加载启用模块,跳过授权表查询 - 普通用户:并发加载 s_module、b_user_module、b_user_power、s_module_power,并组装权限集合 - 主体去重:同一 orgId:userId 重复调用不重复请求,并发共享同一 Promise - 权限判断 - canAccess(moduleCode):判断是否可访问某模块(含父 page 类型模块) - canPower(scopeCode, powerCode):判断是否拥有某功能点(含父 page 类型模块) - 清理机制 - 监听 authStore 的变化(orgId、userId、account、token),在主体变更时清理缓存 - clear 方法区分“进行中请求属于其他主体”的情况,作废旧请求,避免脏数据 ```mermaid flowchart TD LStart(["调用 load()"]) --> CheckAdmin{"是否 g3soft?"} CheckAdmin --> |是| LoadModules["加载启用模块(s_module)"] CheckAdmin --> |否| ValidateUser{"校验 userId"} ValidateUser --> |无效| ThrowErr["抛出错误"] ValidateUser --> |有效| CheckDedup{"是否已加载该主体?"} CheckDedup --> |是| ReturnCached["返回缓存"] CheckDedup --> |否| ParallelLoad["并发加载 4 个接口"] ParallelLoad --> Assemble["组装 modules/grantedPageIds/powerCodes"] Assemble --> MarkLoaded["记录 loadedPrincipalKey"] MarkLoaded --> LEnd(["完成"]) ThrowErr --> LEnd ReturnCached --> LEnd ``` 图表来源 - [permissions.js:41-109](file://fms-vue/src/stores/permissions.js#L41-L109) - [permissions.js:155-182](file://fms-vue/src/stores/permissions.js#L155-L182) 章节来源 - [permissions.js:13-195](file://fms-vue/src/stores/permissions.js#L13-L195) - [permissions.spec.js:59-121](file://fms-vue/tests/permissions.spec.js#L59-L121) ### 路由守卫与状态联动 - 登录态检查:未登录访问受保护路由跳转 /login,带 redirect 参数 - 权限加载:在 beforeEach 中统一调用 permissionStore.load(),确保首屏与业务页挂载前已完成 - 访问控制:若路由 meta.moduleId 存在且无权限,则跳转 /dashboard ```mermaid sequenceDiagram participant Router as "router/index.js" participant Auth as "stores/auth.js" participant Perm as "stores/permissions.js" Router->>Auth : 读取 isAuthenticated alt 未登录 Router-->>Router : 重定向到 /login?redirect=... else 已登录 Router->>Perm : await load() Perm-->>Router : 完成 Router->>Router : 校验 canAccess(moduleId) Router-->>Router : 放行或跳转 /dashboard end ``` 图表来源 - [router/index.js:109-133](file://fms-vue/src/router/index.js#L109-L133) - [permissions.js:131-153](file://fms-vue/src/stores/permissions.js#L131-L153) 章节来源 - [router/index.js:109-133](file://fms-vue/src/router/index.js#L109-L133) ## 依赖关系分析 - main.js - 创建 Pinia 并启用 pinia-plugin-persistedstate - 在挂载前调用 initThemeSync,确保主题同步 - app.js - 依赖 theme/index.js 的 applyDarkMode、applyPrimaryColor - permissions.js - 依赖 services/api.js 的 loadDataApi - 依赖 auth.js 获取当前用户信息 - router/index.js - 依赖 auth.js 与 permissions.js 进行鉴权与权限加载 ```mermaid graph LR M["main.js"] --> A["app.js"] M --> Au["auth.js"] M --> P["permissions.js"] A --> T["theme/index.js"] P --> API["services/api.js"] P --> Au R["router/index.js"] --> Au R --> P ``` 图表来源 - [main.js:14-22](file://fms-vue/src/main.js#L14-L22) - [app.js:1-5](file://fms-vue/src/stores/app.js#L1-L5) - [permissions.js:1-4](file://fms-vue/src/stores/permissions.js#L1-L4) - [router/index.js:1-7](file://fms-vue/src/router/index.js#L1-L7) 章节来源 - [main.js:14-22](file://fms-vue/src/main.js#L14-L22) - [app.js:1-5](file://fms-vue/src/stores/app.js#L1-L5) - [permissions.js:1-4](file://fms-vue/src/stores/permissions.js#L1-L4) - [router/index.js:1-7](file://fms-vue/src/router/index.js#L1-L7) ## 性能考虑 - 并发与去重 - 权限加载对同一主体去重,并发调用共享 Promise,减少重复网络请求 - 管理员提权路径简化请求数量 - 内存与状态清理 - 关闭页签时清理 routeRefreshKeys,避免无限增长 - 权限 store 在主体变更时清理缓存,防止脏数据 - 首屏体验 - 路由守卫在 mount 前完成权限加载,避免菜单“跳出” - 主题同步在挂载前执行,避免首屏闪烁 - 持久化粒度 - 仅持久化必要状态(主题、语言、用户回显),降低存储体积与读写开销 [本节为通用性能建议,无需特定文件引用] ## 故障排查指南 - 登录后菜单空白 - 检查权限 store 是否在路由守卫中被正确 await - 确认主体变化时 clear 不会作废正在进行的请求(同主体) - 主题未生效 - 确认 initThemeSync 已在 Pinia 实例激活后调用 - 检查 applyDarkMode/applyPrimaryColor 是否正确操作 DOM - 登录态异常 - 检查 sessionStorage/localStorage 中的键名与内容 - 确认 clearSession 仅清除会话凭证,不影响用户回显 - 权限判断不准确 - 确认 canAccess/canPower 的 scopeCode 与 powerCode 格式正确 - 检查模块树解析(resolvePageId)是否能找到父 page 类型模块 章节来源 - [permissions.js:155-182](file://fms-vue/src/stores/permissions.js#L155-L182) - [app.js:106-123](file://fms-vue/src/stores/app.js#L106-L123) - [auth.js:22-29](file://fms-vue/src/stores/auth.js#L22-L29) - [router/index.js:109-133](file://fms-vue/src/router/index.js#L109-L133) ## 结论 FMS 前端通过 Pinia 实现了清晰、可维护的状态管理: - 认证、应用、权限三大 store 职责明确,耦合度低 - 通过路由守卫集中管理鉴权与权限加载,保证首屏体验 - 使用 pinia-plugin-persistedstate 实现细粒度的持久化策略 - 完善的测试覆盖验证了关键行为与边界条件 [本节为总结性内容,无需特定文件引用] ## 附录 ### 状态更新最佳实践 - 单一数据源:所有状态变更通过 store actions 进行,组件内不直接修改状态 - 副作用隔离:主题同步、DOM 操作集中在 app store 的初始化与 watch 中 - 幂等与去重:权限加载对同一主体去重,避免重复请求 - 最小持久化:仅持久化必要状态,减少存储压力 - 错误处理:权限加载失败时静默处理,避免阻断路由导航 [本节为通用实践建议,无需特定文件引用] ### 状态调试与测试方法 - 单元测试 - 使用 vitest 与 createTestPinia 创建独立 Pinia 实例 - 模拟 API 响应,验证权限加载与判断逻辑 - 验证持久化键名与内容(sessionStorage/localStorage) - 断言要点 - 认证:setSession/clearSession 的行为与持久化键 - 应用:主题切换、页签管理、刷新计数 - 权限:主体去重、管理员提权、并发请求复用 - 调试技巧 - 在浏览器控制台查看 Pinia state(通过 devtools) - 观察路由守卫日志与 NProgress 进度条,定位加载时机 章节来源 - [stores.spec.js:1-200](file://fms-vue/tests/stores/stores.spec.js#L1-L200) - [permissions.spec.js:1-122](file://fms-vue/tests/permissions.spec.js#L1-L122)