Skip to content

状态管理

项目使用 Pinia 管理用户会话、菜单、界面设置、工作标签和表格偏好。状态设计的重点不是把所有数据放进 Store,而是区分应用状态、用户偏好、页面状态和服务端业务数据。

初始化方式

Store 入口位于 src/store/index.ts,在应用挂载前完成 Pinia 和持久化插件注册:

text
src/main.ts
  -> initStore(app)
  -> 注册 Pinia
  -> 注册持久化插件
  -> 各模块按页面和路由流程使用

页面通过组合式 Store 使用状态:

ts
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/store/modules/user'

const userStore = useUserStore()
const { info, isLogin } = storeToRefs(userStore)

需要保持响应式的状态使用 storeToRefs,方法直接从 Store 实例调用。

主要 Store

src/store/modules 中的模块按领域拆分:

Store职责
userToken、用户、语言、锁屏、搜索记录
menu菜单树、首页路径、路由相关状态
setting布局、主题、圆角、容器与界面开关
worktab已打开标签页、缓存排除、工作区恢复
table表格列显示、顺序和用户偏好
site-settings站点品牌与公共配置
notification通知界面状态
data-screen数据大屏交互状态
page-view-preference页面视图偏好
column-search-history表格列搜索历史及用户隔离

Store 名称和持久化范围并不完全相同。新增模块前应先确认状态是否需要跨页面共享、是否需要刷新恢复,以及是否包含账号敏感数据。

状态分层

应用级状态

适合放入 Store:

  • 当前用户和登录状态
  • 菜单与动态路由关联状态
  • 主题、布局、语言
  • 工作标签和全局通知
  • 跨页面复用的用户偏好

页面级状态

优先保留在页面或 Hook:

  • 当前弹窗是否打开
  • 单个页面的临时筛选条件
  • 表单草稿
  • 当前请求的 loading 和 error

业务数据

订单、客户、审批记录等服务端数据不应因为“多个页面可能使用”就长期放进持久化 Store。优先通过 API 按需获取;确实需要缓存时,应定义失效策略、账号隔离和刷新条件。

持久化键

存储配置位于:

text
src/utils/storage/storage-config.ts
src/utils/storage/storage-key-manager.ts

版本化键格式:

text
{namespace}-v{version}-{storeId}

例如:

text
acme-admin-v1.0.0-user
acme-admin-v1.0.0-setting

命名空间来自:

ini
VITE_STORAGE_NAMESPACE = acme-admin

每个客户项目都应使用独立命名空间。配置方式见环境变量

版本迁移

StorageKeyManager 获取当前版本键时会:

text
检查当前版本数据
  -> 存在:直接使用
  -> 不存在:查找其他版本同名 Store
  -> 找到:复制到当前版本键
  -> 返回当前版本键

该机制用于保留常规版本升级中的用户偏好,但它不是无限期的数据迁移框架。Store 结构发生破坏性变化时,仍应编写明确的数据兼容逻辑,或只清理受影响的键。

不要使用 localStorage.clear()

清空整个 LocalStorage 会删除同域名下其他系统的数据。应通过 StorageConfig 生成的命名空间和 Store 键精确处理目标数据。

用户隔离

项目对容易跨账号泄漏的状态执行额外清理:

  • 退出时清理 Token、用户信息和动态路由。
  • 表格列偏好在账号切换时清理。
  • 列搜索历史按用户归属管理。
  • 搜索状态记录当前所属用户,切换账号时重置。
  • 工作标签根据上次用户判断是否保留。

新增持久化状态时,应回答三个问题:

  1. 这个状态是否属于具体用户?
  2. 切换账号时能否被另一个用户看到?
  3. 退出登录时应该保留、清理还是按用户分区?

创建新 Store

ts
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'

export const useWorkspaceStore = defineStore(
  'workspaceStore',
  () => {
    const density = ref<'compact' | 'default'>('default')
    const isCompact = computed(() => density.value === 'compact')

    const setDensity = (value: 'compact' | 'default') => {
      density.value = value
    }

    return { density, isCompact, setDensity }
  },
  {
    persist: {
      pick: ['density']
    }
  }
)

建议:

  • Store ID 保持稳定,避免无意生成新的存储键。
  • 只持久化需要刷新恢复的字段。
  • 派生值使用 computed,不要重复持久化。
  • 异步请求失败时保留明确的 error 状态。
  • 不在 Store 中直接操作页面 DOM。

Store 与 API

Store 负责状态,API 模块负责 HTTP 接口。以登录为例:

text
src/api/auth.ts:定义登录、退出和当前用户接口
userStore:保存 Token、用户、登录状态和退出清理
路由守卫:编排会话恢复和菜单初始化

如果一个 Store 开始同时包含大量 URL、请求方法和响应字段适配,说明请求与状态职责已经混在一起,应把 HTTP 接口迁移到 src/api。只有跨多个 API 的复杂流程才考虑使用 Service 编排,Service 不能替代 API 目录。

调试与维护

  • 使用 Vue DevTools 查看 Store 的实时变化。
  • 检查 LocalStorage 键是否使用正确命名空间。
  • 修改持久化字段后验证旧版本数据迁移。
  • 测试登录、退出、刷新和账号切换四条路径。
  • 不要直接编辑持久化 JSON 作为正式迁移方案。

登录态恢复见登录与会话,页面数据来源的边界见本地数据模式

根据 MIT 许可证发布