Skip to content

状态管理

CondorAdmin 使用 Pinia 作为状态管理库,采用模块化设计,将不同业务的状态拆分为独立的 Store。

Pinia 架构

Store 模块

src/store/
├── index.ts                 # Store 入口
├── plugins/                 # Store 插件
│   └── index.ts
└── modules/
    ├── auth/                # 认证模块
    │   ├── index.ts
    │   └── shared.ts
    ├── route/               # 路由模块
    │   ├── index.ts
    │   └── shared.ts
    ├── tab/                 # 标签页模块
    │   ├── index.ts
    │   └── shared.ts
    ├── theme/               # 主题模块
    │   ├── index.ts
    │   └── shared.ts
    ├── app/                 # 应用模块
    │   └── index.ts
    ├── captcha/             # 验证码模块
    │   └── index.ts
    └── condor/              # Condor 业务模块
        └── index.ts

Store ID 枚举

typescript
// src/enum/index.ts
export enum SetupStoreId {
  Auth = 'auth-store',
  Route = 'route-store',
  Tab = 'tab-store',
  Theme = 'theme-store',
  App = 'app-store',
  Captcha = 'captcha-store',
  Condor = 'condor-store'
}

核心 Store

1. useAuthStore(认证模块)

职责:管理用户登录状态、用户信息、Token、权限列表。

位置src/store/modules/auth/index.ts

状态

typescript
const token = ref<string>('');        // 访问 Token
const userInfo = reactive({           // 用户信息
  userId: '',
  username: '',
  email: '',
  avatar: '',
  roles: [],                          // 角色列表
  buttons: []                         // 按钮权限列表
});
const isLogin = computed(() => Boolean(token.value));

核心方法

login() - 登录

typescript
async function login(params: Api.Auth.LoginParams, redirect = true) {
  startLoading();
  
  // 1. 获取公钥
  const { data } = await fetchGetPublicKey();
  
  // 2. RSA 加密密码
  const jse = new JSEncrypt();
  jse.setPublicKey(data.publicKey);
  const encryptedPassword = jse.encrypt(params.password);
  
  // 3. 登录请求
  const { data: loginToken, error } = await fetchLogin({
    ...params,
    password: encryptedPassword
  });
  
  if (!error) {
    // 4. 存储 Token
    localStg.set('token', loginToken.access_token);
    
    // 5. 获取用户信息
    await getUserInfo();
    
    // 6. 重定向
    if (redirect) {
      await redirectFromLogin();
    }
  }
  
  endLoading();
}

getUserInfo() - 获取用户信息

typescript
async function getUserInfo() {
  const { data: info, error } = await fetchGetUserInfo();
  
  if (!error) {
    // 更新用户信息
    Object.assign(userInfo, info);
    
    // 启动 SSE
    if (status.value !== 'open') {
      start();
    }
    
    // 初始化字典
    if (!condorStore.status) {
      condorStore.initDict();
    }
    
    return true;
  }
  
  return false;
}

hasPermission() - 权限判断

typescript
function hasPermission(permission: string | string[]) {
  if (!permission) return true;
  
  const roles = [...userInfo.buttons, ...userInfo.roles];
  
  // 超级管理员
  if (roles.includes('*')) return true;
  
  // 数组权限(与关系)
  if (Array.isArray(permission)) {
    return permission.every(item => roles.includes(item));
  }
  
  // 单个权限
  return roles.includes(permission);
}

resetStore() - 重置状态

typescript
async function resetStore() {
  clearAuthStorage();
  authStore.$reset();
  
  if (!route.meta.constant) {
    await toLogin();
  }
  
  tabStore.cacheTabs();
  routeStore.resetStore();
}

使用示例

vue
<script setup lang="ts">
import { useAuthStore } from '@/store';

const authStore = useAuthStore();

// 登录
const handleLogin = async () => {
  await authStore.login({
    username: 'admin',
    password: '123456',
    captcha: '1234'
  });
};

// 权限判断
const canAdd = authStore.hasPermission('admin:add');
const canEdit = authStore.hasPermission(['admin:edit', 'admin:view']);
</script>

2. useRouteStore(路由模块)

职责:管理动态路由、菜单树、面包屑。

位置src/store/modules/route/index.ts

状态

typescript
const authRouteMode = ref<ImportMetaEnv['VITE_AUTH_ROUTE_MODE']>('static');
const isInitAuthRoute = ref(false);     // 是否已初始化路由
const menus = ref<App.Global.Menu[]>([]); // 菜单树
const searchMenus = computed(() => {    // 扁平化菜单(用于搜索)
  return flattenMenus(menus.value);
});

核心方法

initAuthRoute() - 初始化权限路由

typescript
async function initAuthRoute() {
  if (isInitAuthRoute.value) return;
  
  // 1. 获取用户路由
  const routes = await getUserRoutes();
  
  // 2. 添加到 Router
  routes.forEach(route => {
    router.addRoute(route);
  });
  
  // 3. 生成菜单树
  menus.value = transformMenus(routes);
  
  isInitAuthRoute.value = true;
}

getSelectedMenuKeyPath() - 获取当前菜单路径

typescript
function getSelectedMenuKeyPath(selectedKey: string) {
  const menuKey = selectedKey || activeMenu.value;
  const path: string[] = [];
  
  function findPath(menus: App.Global.Menu[], key: string) {
    for (const menu of menus) {
      if (menu.key === key) {
        path.unshift(menu.key);
        return true;
      }
      if (menu.children?.length) {
        if (findPath(menu.children, key)) {
          path.unshift(menu.key);
          return true;
        }
      }
    }
    return false;
  }
  
  findPath(menus.value, menuKey);
  return path;
}

3. useTabStore(标签页模块)

职责:管理多标签页状态、缓存页面。

位置src/store/modules/tab/index.ts

状态

typescript
const tabs = ref<App.Global.Tab[]>([]);          // 标签页列表
const activeTab = ref<string>('');               // 当前激活标签
const cacheRoutes = ref<string[]>([]);           // 缓存的路由名称

核心方法

addTab() - 添加标签页

typescript
function addTab(route: RouteLocationNormalizedLoaded) {
  const tab: App.Global.Tab = {
    id: route.name as string,
    label: route.meta.title || '',
    routeName: route.name as string,
    routePath: route.path,
    fullPath: route.fullPath,
    fixedIndex: route.meta.fixedIndexInTab,
    icon: route.meta.icon
  };
  
  const index = tabs.value.findIndex(item => item.id === tab.id);
  
  if (index === -1) {
    tabs.value.push(tab);
  } else {
    tabs.value[index] = tab;
  }
  
  activeTab.value = tab.id;
  
  // 添加到缓存
  if (route.meta.keepAlive) {
    addCacheRoute(route.name as string);
  }
}

removeTab() - 移除标签页

typescript
async function removeTab(tabId: string) {
  const index = tabs.value.findIndex(tab => tab.id === tabId);
  if (index === -1) return;
  
  const isActiveTab = activeTab.value === tabId;
  const tab = tabs.value[index];
  
  tabs.value.splice(index, 1);
  removeCacheRoute(tab.routeName);
  
  // 如果关闭的是当前标签,跳转到相邻标签
  if (isActiveTab && tabs.value.length > 0) {
    const nextTab = tabs.value[index] || tabs.value[index - 1];
    await router.push(nextTab.fullPath);
  }
}

clearTabs() - 清空标签页

typescript
function clearTabs() {
  tabs.value = [];
  cacheRoutes.value = [];
}

cacheTabs() - 持久化标签页

typescript
function cacheTabs() {
  localStg.set('globalTabs', tabs.value);
}

4. useThemeStore(主题模块)

职责:管理主题配置、暗黑模式、布局设置。

位置src/store/modules/theme/index.ts

状态

typescript
const settings = ref<App.Theme.ThemeSetting>(initThemeSettings());
const darkMode = computed(() => {
  if (settings.value.themeScheme === 'auto') {
    return osTheme.value === 'dark';
  }
  return settings.value.themeScheme === 'dark';
});
const themeColors = computed(() => ({
  primary: settings.value.themeColor,
  ...settings.value.otherColor
}));

核心配置

typescript
interface ThemeSetting {
  themeScheme: 'light' | 'dark' | 'auto';  // 主题模式
  themeColor: string;                       // 主题色
  otherColor: {
    info: string;
    success: string;
    warning: string;
    error: string;
  };
  layout: {
    mode: 'vertical' | 'horizontal';        // 布局模式
    scrollMode: 'content' | 'wrapper';      // 滚动模式
  };
  page: {
    animate: boolean;                       // 页面切换动画
    animateMode: 'fade' | 'fade-slide';     // 动画模式
  };
  header: {
    height: number;                         // 头部高度
    breadcrumb: {
      visible: boolean;                     // 显示面包屑
      showIcon: boolean;                    // 显示图标
    };
  };
  tab: {
    visible: boolean;                       // 显示标签页
    height: number;                         // 标签页高度
    mode: 'chrome' | 'button';              // 标签页样式
  };
  sider: {
    width: number;                          // 侧边栏宽度
    collapsedWidth: number;                 // 折叠宽度
  };
  footer: {
    visible: boolean;                       // 显示页脚
    fixed: boolean;                         // 固定页脚
    height: number;                         // 页脚高度
  };
}

核心方法

setThemeScheme() - 设置主题模式

typescript
function setThemeScheme(scheme: 'light' | 'dark' | 'auto') {
  settings.value.themeScheme = scheme;
  toggleCssDarkMode(darkMode.value);
}

setThemeColor() - 设置主题色

typescript
function setThemeColor(color: string) {
  settings.value.themeColor = color;
  addThemeVarsToGlobal(themeColors.value);
}

5. useAppStore(应用模块)

职责:管理全局应用状态(侧边栏折叠、移动端等)。

位置src/store/modules/app/index.ts

状态

typescript
const siderCollapse = ref(false);           // 侧边栏折叠
const isMobile = ref(false);                // 是否移动端
const contentFullScreen = ref(false);       // 内容全屏
const fullScreen = ref(false);              // 全屏模式

核心方法

typescript
function toggleSiderCollapse() {
  siderCollapse.value = !siderCollapse.value;
}

function toggleContentFullScreen() {
  contentFullScreen.value = !contentFullScreen.value;
}

6. useCondorStore(Condor 业务模块)

职责:管理字典数据、系统配置等业务状态。

位置src/store/modules/condor/index.ts

状态

typescript
const dictData = ref<Record<string, App.Dict[]>>({});  // 字典数据
const config = ref<Record<string, any>>({});           // 系统配置
const status = ref(false);                             // 是否已初始化

核心方法

initDict() - 初始化字典

typescript
async function initDict() {
  const { data, error } = await fetchDictData();
  
  if (!error && data) {
    dictData.value = data;
    status.value = true;
  }
}

getDict() - 获取字典

typescript
function getDict(code: string): App.Dict[] {
  return dictData.value[code] || [];
}

getDictLabel() - 获取字典标签

typescript
function getDictLabel(code: string, value: string | number): string {
  const dict = getDict(code).find(item => item.value === value);
  return dict?.label || '';
}

使用示例

vue
<script setup lang="ts">
import { useCondorStore } from '@/store';

const condorStore = useCondorStore();

// 获取字典
const statusDict = condorStore.getDict('status');
// [{ label: '启用', value: 1 }, { label: '禁用', value: 0 }]

// 获取字典标签
const statusLabel = condorStore.getDictLabel('status', 1);
// '启用'
</script>

Store 组合

跨 Store 调用

typescript
import { useAuthStore } from './auth';
import { useRouteStore } from './route';

export const useAppStore = defineStore('app', () => {
  const authStore = useAuthStore();
  const routeStore = useRouteStore();
  
  function init() {
    authStore.initUserInfo();
    routeStore.initAuthRoute();
  }
  
  return { init };
});

响应式依赖

typescript
// 监听其他 Store 的状态变化
watch(
  () => authStore.isLogin,
  (isLogin) => {
    if (isLogin) {
      routeStore.initAuthRoute();
    }
  }
);

持久化

LocalStorage 持久化

typescript
// shared.ts
import { localStg } from '@/utils/storage';

export function getToken() {
  return localStg.get('token') || '';
}

export function clearAuthStorage() {
  localStg.remove('token');
  localStg.remove('refreshToken');
}

自动持久化插件

typescript
// src/store/plugins/index.ts
import type { PiniaPluginContext } from 'pinia';

export function piniaPlugin(context: PiniaPluginContext) {
  const { store } = context;
  
  // 监听状态变化,自动持久化
  store.$subscribe(() => {
    if (store.$id === 'theme-store') {
      localStg.set('theme-settings', store.settings);
    }
  });
}

最佳实践

1. 使用 Setup Store

typescript
// ✅ 推荐:Setup Store(Composition API 风格)
export const useAuthStore = defineStore('auth', () => {
  const token = ref('');
  function login() { }
  return { token, login };
});

// ❌ 不推荐:Options Store
export const useAuthStore = defineStore('auth', {
  state: () => ({ token: '' }),
  actions: { login() { } }
});

2. 状态拆分

typescript
// ✅ 按职责拆分多个 Store
useAuthStore()    // 认证
useRouteStore()   // 路由
useTabStore()     // 标签页

// ❌ 单个巨大的 Store
useGlobalStore()  // 所有状态

3. 避免直接修改状态

typescript
// ✅ 通过方法修改
authStore.login(params);

// ❌ 直接修改
authStore.token = 'abc';

4. 类型定义

typescript
// 定义 Store 类型
export type AuthStore = ReturnType<typeof useAuthStore>;

// 使用类型
function useAuth(): AuthStore {
  return useAuthStore();
}

调试

Vue DevTools

安装 Vue DevTools 扩展,可查看所有 Store 的状态和历史记录。

日志

typescript
// 在 Store 中打印日志
console.log('[AuthStore] Login success', userInfo);

相关文档