Skip to content

状态管理 ​

概述 ​

前端状态管理基于 Pinia 构建,所有全局状态集中放在 packages/stores 这个共享包中,应用层(apps/web-ele)再通过 src/store 细化业务状态。状态在浏览器中按「是否敏感」分别做本地持久化,保证刷新页面后登录态、标签页等不丢失。

Store 体系总览 ​

packages/stores 导出了 5 个核心 store,统一以 core- 为命名前缀,避免多应用之间缓存冲突。

模块Store主要职责
access.tsuseAccessStore访问令牌、刷新令牌、权限码、可访问菜单与路由、锁屏、租户
user.tsuseUserStore当前用户信息、用户角色
dict.tsuseDictStore字典数据缓存,按 dictType 查询
tabbar.tsuseTabbarStore多标签页的增删改、缓存、固定、拖拽、右键菜单
timezone.tsuseTimezoneStore时区选择与 dayjs 默认时区同步

应用层额外提供 apps/web-ele/src/store/auth.ts 中的 useAuthStore,它把登录、登出、拉取用户权限信息串起来,并负责写入 access / user 两个核心 store,是「状态」与「业务动作」的桥接层。

Pinia 初始化与持久化 ​

状态容器在应用启动阶段由 initStores 完成装配(packages/stores/src/setup.ts):

ts
// apps/web-ele/src/bootstrap.ts
await initStores(app, { namespace });

初始化的关键点:

  • 创建 Pinia 实例并 app.use(pinia)。
  • 通过 pinia-plugin-persistedstate 注册持久化插件,持久化 key 统一加上 namespace- 前缀,便于多应用隔离。
  • 持久化存储做了安全处理:生产环境使用 secure-ls 以 AES 加密 + 压缩 写入(密钥取自 VITE_APP_STORE_SECURE_KEY),开发环境则直接落 localStorage 方便调试。

不同 store 对自身状态做了差异化的持久化策略:

  • access、dict、timezone、auth 中需要跨刷新保留的字段(如令牌、权限码、字典缓存)持久化到 localStorage(生产环境加密)。
  • tabbar 的标签页与访问历史持久化到 sessionStorage(关闭浏览器即清空),避免泄露历史记录。

登出或需要清空全部状态时调用 resetAllStores(),它会遍历并 $reset 所有已实例化的 store:

ts
import { resetAllStores } from '@vben/stores';

// 登出
resetAllStores();
accessStore.setLoginExpired(false);

access 权限与令牌 ​

useAccessStore 是权限系统的核心,保存了登录态与可访问资源。它同时承担「令牌」和「权限资源」两类数据。

主要状态:

  • accessToken / refreshToken:登录令牌,由登录流程写入并持久化。
  • accessCodes:权限码数组,用于按钮级鉴权(配合 useAccess / v-access,见「权限集成」)。
  • accessMenus / accessRoutes:后端返回的可访问菜单与经 generateAccess 过滤后的路由。
  • isAccessChecked:是否已生成动态路由,路由守卫据此判断是否重新生成。
  • isLockScreen / lockScreenPassword:锁屏状态。
  • tenantId / visitTenantId:登录与访问的租户编号(多租户场景)。
  • loginExpired:登录是否过期,用于触发重新登录提示。

持久化字段只包含令牌、权限码、租户、锁屏等必要数据,菜单与路由不持久化(每次启动由路由守卫重新生成)。

ts
import { useAccessStore } from '@vben/stores';

const accessStore = useAccessStore();
// 写入令牌
accessStore.setAccessToken(token);
// 读取权限码
const codes = accessStore.accessCodes;

user 用户信息 ​

useUserStore 保存当前登录用户的资料与角色,数据来自 getAuthPermissionInfoApi 的返回。

  • userInfo:用户基本信息(头像、昵称、用户名、邮箱、userId 等)。
  • userRoles:用户角色标识列表,供 generateAccess 做菜单/路由过滤。

该 store 默认不持久化,登录后由 fetchUserInfo 写入;登出时随 resetAllStores 清空。

auth 登录业务 Store ​

useAuthStore(apps/web-ele/src/store/auth.ts)是状态与业务动作的枢纽,它组合了 access 与 user 两个核心 store,对外暴露:

  • authLogin(type, params, onSuccess):处理账号密码、短信、社交、注册等多种登录方式,成功后写入令牌并拉取用户信息。
  • fetchUserInfo():调用权限信息接口,把 user、roles、menus、permissions 分别写入 user / access store。
  • logout(redirect):调用登出接口,清空全部 store 并跳转登录页。
  • loginLoading:登录按钮的加载态。
ts
import { useAuthStore } from '#/store';

const authStore = useAuthStore();
await authStore.authLogin('username', { username, password });

典型的登录态流转如下:

dict 字典缓存 ​

useDictStore 提供全局字典缓存,避免每个页面重复请求字典接口。

  • setDictCache(dicts):直接覆盖缓存。
  • setDictCacheByApi(api, params?, labelField?, valueField?):通过接口拉取并按 dictType 自动分组缓存(路由守卫在首次进入时调用 getSimpleDictDataList 预热)。
  • getDictOptions(dictType) / getDictData(dictType, value):按类型读取选项列表或单个字典项。
ts
import { useDictStore } from '@vben/stores';

const dictStore = useDictStore();
// 业务组件内同步读取字典选项
const options = dictStore.getDictOptions('sys_normal_disable');

tabbar 多标签页 ​

useTabbarStore 管理顶部的多页签,支持新增、关闭(当前/左侧/右侧/其他/全部)、固定(affix)、刷新、拖拽排序、新窗口打开,以及基于 keepAlive 的路由缓存。

常用能力:

  • addTab(routeTab):路由切换时自动加入标签。
  • closeTab(tab, router) / closeAllTabs / closeLeftTabs / closeRightTabs / closeOtherTabs:各类关闭操作。
  • refresh(router) / refreshByName(name):刷新当前或指定页签。
  • pinTab / unpinTab / toggleTabPin:固定与取消固定。
  • getTabs / getCachedTabs / getExcludeCachedTabs:供布局组件渲染标签栏与 <router-view> 缓存使用。

标签状态持久化在 sessionStorage,刷新页面后仍保留已打开的标签(含访问历史 visitHistory)。

timezone 时区 ​

useTimezoneStore 维护当前时区,并与 dayjs 的默认时区保持同步,确保全站时间展示一致。

  • timezone:当前时区响应式引用。
  • initTimezone():初始化时区。
  • setTimezone(tz):切换时区并同步 dayjs。
  • getTimezoneOptions():获取可选时区列表;可通过 setTimezoneHandler 注入自定义数据源(如后端下发)。
ts
import { useTimezoneStore } from '@vben/stores';

const timezoneStore = useTimezoneStore();
await timezoneStore.setTimezone('Asia/Shanghai');

在组件中使用 ​

在 Vue 组件中通过对应的 useXxxStore() 获取实例,状态直接通过属性访问;需要保持响应式的状态在模板或计算属性中使用 storeToRefs 解构。

vue
<script setup lang="ts">
import { computed } from 'vue';
import { storeToRefs, useUserStore } from '@vben/stores';

const userStore = useUserStore();
// 解构保持响应式
const { userInfo } = storeToRefs(userStore);
// 计算属性
const nickname = computed(() => userInfo.value?.nickname ?? '');
</script>

<template>
  <span>{{ nickname }}</span>
</template>

要点:

  • 读取/修改状态直接用 store.xxx,Pinia 会自动追踪依赖。
  • 需要把 state 作为 ref 传给子组件或放进 computed 时,使用 storeToRefs 而非直接解构,否则会丢失响应性。
  • 修改状态推荐走 store 暴露的 action(如 setUserInfo),便于统一维护与调试。
  • 所有核心 store 都开启了 HMR(acceptHMRUpdate),开发时修改不会丢失状态。

与权限、路由的衔接 ​

状态管理是权限与路由的数据基座:登录后 auth store 把权限码与菜单写入 access store,路由守卫 setupAccessGuard 读取 accessCodes / accessMenus,结合 generateAccess 生成可访问路由,并把结果回写到 access store。按钮级鉴权(useAccess / v-access)则直接读取 accessStore.accessCodes。三者关系见「权限集成」与「路由与菜单」章节。