外观
状态管理
概述
前端状态管理基于 Pinia 构建,所有全局状态集中放在 packages/stores 这个共享包中,应用层(apps/web-ele)再通过 src/store 细化业务状态。状态在浏览器中按「是否敏感」分别做本地持久化,保证刷新页面后登录态、标签页等不丢失。
Store 体系总览
packages/stores 导出了 5 个核心 store,统一以 core- 为命名前缀,避免多应用之间缓存冲突。
| 模块 | Store | 主要职责 |
|---|---|---|
access.ts | useAccessStore | 访问令牌、刷新令牌、权限码、可访问菜单与路由、锁屏、租户 |
user.ts | useUserStore | 当前用户信息、用户角色 |
dict.ts | useDictStore | 字典数据缓存,按 dictType 查询 |
tabbar.ts | useTabbarStore | 多标签页的增删改、缓存、固定、拖拽、右键菜单 |
timezone.ts | useTimezoneStore | 时区选择与 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/accessstore。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。三者关系见「权限集成」与「路由与菜单」章节。