状态管理
项目使用 Pinia 管理用户会话、菜单、界面设置、工作标签和表格偏好。状态设计的重点不是把所有数据放进 Store,而是区分应用状态、用户偏好、页面状态和服务端业务数据。
初始化方式
Store 入口位于 src/store/index.ts,在应用挂载前完成 Pinia 和持久化插件注册:
src/main.ts
-> initStore(app)
-> 注册 Pinia
-> 注册持久化插件
-> 各模块按页面和路由流程使用页面通过组合式 Store 使用状态:
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 | 职责 |
|---|---|
user | Token、用户、语言、锁屏、搜索记录 |
menu | 菜单树、首页路径、路由相关状态 |
setting | 布局、主题、圆角、容器与界面开关 |
worktab | 已打开标签页、缓存排除、工作区恢复 |
table | 表格列显示、顺序和用户偏好 |
site-settings | 站点品牌与公共配置 |
notification | 通知界面状态 |
data-screen | 数据大屏交互状态 |
page-view-preference | 页面视图偏好 |
column-search-history | 表格列搜索历史及用户隔离 |
Store 名称和持久化范围并不完全相同。新增模块前应先确认状态是否需要跨页面共享、是否需要刷新恢复,以及是否包含账号敏感数据。
状态分层
应用级状态
适合放入 Store:
- 当前用户和登录状态
- 菜单与动态路由关联状态
- 主题、布局、语言
- 工作标签和全局通知
- 跨页面复用的用户偏好
页面级状态
优先保留在页面或 Hook:
- 当前弹窗是否打开
- 单个页面的临时筛选条件
- 表单草稿
- 当前请求的 loading 和 error
业务数据
订单、客户、审批记录等服务端数据不应因为“多个页面可能使用”就长期放进持久化 Store。优先通过 API 按需获取;确实需要缓存时,应定义失效策略、账号隔离和刷新条件。
持久化键
存储配置位于:
src/utils/storage/storage-config.ts
src/utils/storage/storage-key-manager.ts版本化键格式:
{namespace}-v{version}-{storeId}例如:
acme-admin-v1.0.0-user
acme-admin-v1.0.0-setting命名空间来自:
VITE_STORAGE_NAMESPACE = acme-admin每个客户项目都应使用独立命名空间。配置方式见环境变量。
版本迁移
StorageKeyManager 获取当前版本键时会:
检查当前版本数据
-> 存在:直接使用
-> 不存在:查找其他版本同名 Store
-> 找到:复制到当前版本键
-> 返回当前版本键该机制用于保留常规版本升级中的用户偏好,但它不是无限期的数据迁移框架。Store 结构发生破坏性变化时,仍应编写明确的数据兼容逻辑,或只清理受影响的键。
不要使用 localStorage.clear()
清空整个 LocalStorage 会删除同域名下其他系统的数据。应通过 StorageConfig 生成的命名空间和 Store 键精确处理目标数据。
用户隔离
项目对容易跨账号泄漏的状态执行额外清理:
- 退出时清理 Token、用户信息和动态路由。
- 表格列偏好在账号切换时清理。
- 列搜索历史按用户归属管理。
- 搜索状态记录当前所属用户,切换账号时重置。
- 工作标签根据上次用户判断是否保留。
新增持久化状态时,应回答三个问题:
- 这个状态是否属于具体用户?
- 切换账号时能否被另一个用户看到?
- 退出登录时应该保留、清理还是按用户分区?
创建新 Store
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 接口。以登录为例:
src/api/auth.ts:定义登录、退出和当前用户接口
userStore:保存 Token、用户、登录状态和退出清理
路由守卫:编排会话恢复和菜单初始化如果一个 Store 开始同时包含大量 URL、请求方法和响应字段适配,说明请求与状态职责已经混在一起,应把 HTTP 接口迁移到 src/api。只有跨多个 API 的复杂流程才考虑使用 Service 编排,Service 不能替代 API 目录。
调试与维护
- 使用 Vue DevTools 查看 Store 的实时变化。
- 检查 LocalStorage 键是否使用正确命名空间。
- 修改持久化字段后验证旧版本数据迁移。
- 测试登录、退出、刷新和账号切换四条路径。
- 不要直接编辑持久化 JSON 作为正式迁移方案。
