Skip to content

🎨 前端技术架构

Ape-Volo-Admin 前端采用业界领先的技术栈构建,基于 Vue 3 渐进式框架,结合 Vite 6 极速构建工具与 Element Plus 企业级组件库,为开发者提供现代化、高效能的开发体验,为用户呈现流畅优雅的交互界面。

🚀 核心技术栈

核心框架

技术版本说明
Vue 33.x渐进式 JavaScript 框架,采用 Composition API 提供更灵活的代码组织方式
Vite 66.x下一代前端构建工具,利用原生 ES 模块实现极速冷启动与热更新
Element Plus2.x基于 Vue 3 的企业级 UI 组件库,提供丰富的高质量组件

状态与路由

技术说明
PiniaVue 3 官方推荐的新一代状态管理方案,轻量级且类型安全
Vue Router 4Vue.js 官方路由管理器,支持动态路由和权限控制

开发工具链

工具用途
TypeScriptJavaScript 超集,提供静态类型检查(可选)
ESLint代码质量与规范检查工具
Prettier代码格式化工具,统一代码风格
Sass/SCSSCSS 预处理器,增强样式编写能力

性能优化特性

  • 插件生态 - 丰富的 Vite 插件支持,扩展构建能力
  • Tree Shaking - 智能移除未使用代码,减小打包体积
  • Code Splitting - 按需加载,优化首屏加载性能
  • HMR - 热模块替换,开发时保持应用状态的即时更新

📁 项目目录结构

ape-volo-web/
├── public/                          # 静态资源目录(不经过构建工具处理)
│   ├── favicon.ico                  # 网站图标
│   └── logo.png                     # 应用 Logo

├── src/                             # 源代码目录
│   ├── api/                         # API 接口层 - 统一管理后端接口调用
│   │
│   ├── assets/                      # 资源文件 - 经过构建工具处理的静态资源
│   │   ├── images/                  # 图片资源(*.png, *.jpg, *.svg)
│   │   └── icons/                   # SVG 图标库
│   │
│   ├── components/                  # 公共组件 - 可复用的 UI 组件
│   │
│   ├── directive/                   # 自定义指令 - Vue 指令扩展
│   │   ├── index.js                 # 指令统一注册入口
│   │   └── permission/              # 权限控制指令模块
│   │       └── index.js             # v-has-role、v-has-perm 等权限指令
│   │
│   ├── enums/                       # 枚举常量 - 业务枚举值定义
│   │
│   ├── hooks/                       # Composition API 组合式函数
│   │   └── responsive.js            # 响应式布局监听 Hook
│   │
│   ├── pinia/                       # Pinia 状态管理
│   │   └── modules/                 # 模块化 Store
│   │       ├── app.js               # 应用全局状态
│   │       ├── user.js              # 用户信息状态
│   │       ├── router.js            # 路由状态
│   │       └── dictionary.js        # 字典数据状态
│   │
│   ├── plugins/                     # Vue 插件模块
│   │
│   ├── router/                      # 路由配置
│   │   └── index.js                 # 路由核心配置与导航守卫
│   │
│   ├── style/                       # 全局样式文件
│   │   └── element/                 # Element Plus 主题定制
│   │
│   ├── utils/                       # 工具函数库
│   │   ├── request.js               # Axios 请求封装
│   │   └── asyncRouter.js           # 动态路由处理工具
│   │
│   ├── views/                       # 页面组件 - 路由级别的视图组件
│   │
│   ├── App.vue                      # 应用根组件
│   ├── main.js                      # 应用入口文件 - Vue 应用初始化
│   └── setting.js                   # 全局配置常量

├── .env.development                 # 开发环境变量配置
├── .env.production                  # 生产环境变量配置
├── babel.config.js                  # Babel 编译配置
├── componentNameMap.js              # 组件名称映射表
├── eslint.config.mjs                # ESLint 代码规范配置
├── index.html                       # HTML 入口模板
├── jsconfig.json                    # JavaScript 项目配置
├── package.json                     # 项目依赖与脚本配置
├── postcss.config.js                # PostCSS 配置
├── tailwind.config.js               # Tailwind CSS 配置
└── vite.config.js                   # Vite 构建配置

🛠️ 开发环境配置

环境要求

工具版本要求说明
Node.js>= 18.0.0JavaScript 运行时环境
包管理器npm >= 8.0.0 或 yarn >= 1.22.0依赖包管理工具
Git最新稳定版版本控制系统

快速开始

1. 安装依赖

bash
# 进入前端项目目录
cd ape-volo-web

# 使用 npm 安装项目依赖
npm install

# 或使用 yarn(推荐,速度更快)
yarn install

2. 常用命令

bash
# 🚀 启动开发服务器(支持热更新)
npm run dev
# 或
yarn dev

# 📦 构建生产版本(代码压缩优化)
npm run build
# 或
yarn build

# 🔍 代码质量检查
npm run lint
# 或
yarn lint

# ✨ 代码格式化(统一代码风格)
npm run format
# 或
yarn format

🎯 核心配置详解

Vite 构建配置

配置文件: vite.config.js

此配置文件是 Vite 构建工具的核心,定义了开发服务器、构建优化、插件等关键配置。

javascript
export default ({ mode }) => {
  const NODE_ENV = mode || "development";
  const envFiles = [`.env.${NODE_ENV}`];
  for (const file of envFiles) {
    const envConfig = dotenv.parse(fs.readFileSync(file));
    for (const k in envConfig) {
      process.env[k] = envConfig[k];
    }
  }

  const timestamp = Date.parse(new Date());

  const optimizeDeps = {};

  const alias = {
    "@": path.resolve(__dirname, "./src"),
    vue$: "vue/dist/vue.runtime.esm-bundler.js",
  };

  const esbuild = {};

  const rollupOptions = {
    output: {
      entryFileNames: "assets/A92E5D4B7C1F8E3A6B4C9D2F0E7A1B3C[name].[hash].js",
      chunkFileNames: "assets/A92E5D4B7C1F8E3A6B4C9D2F0E7A1B3C[name].[hash].js",
      assetFileNames:
        "assets/A92E5D4B7C1F8E3A6B4C9D2F0E7A1B3C[name].[hash].[ext]",
    },
  };

  const base = "/";
  const root = "./";
  const outDir = "dist";

  const config = {
    base: base, // 编译后js导入的资源路径
    root: root, // index.html文件所在位置
    publicDir: "public", // 静态资源文件夹
    resolve: {
      alias,
    },
    define: {
      "process.env": {},
    },
    css: {
      preprocessorOptions: {
        scss: {
          api: "modern-compiler", // or "modern"
        },
      },
      postcss: "./postcss.config.js",
    },
    server: {
      open: true,
      port: process.env.VITE_CLI_PORT,
      proxy: {
        [process.env.VITE_BASE_API]: {
          // 需要代理的路径   例如 '/api'
          target: `${process.env.VITE_BASE_PATH}:${process.env.VITE_SERVER_PORT}/`,
          changeOrigin: true,
          rewrite: (path) =>
            path.replace(new RegExp("^" + process.env.VITE_BASE_API), ""),
        },
      },
    },
    build: {
      minify: "terser", // 是否进行压缩,boolean | 'terser' | 'esbuild',默认使用terser
      manifest: false, // 是否产出manifest.json
      sourcemap: false, // 是否产出sourcemap.json
      outDir: outDir, // 产出目录
      terserOptions: {
        compress: {
          //生产环境时移除console
          drop_console: true,
          drop_debugger: true,
        },
      },
      rollupOptions,
    },
    esbuild,
    optimizeDeps,
    plugins: [
      process.env.VITE_POSITION === "open" &&
        vueDevTools({ launchEditor: process.env.VITE_EDITOR }),
      vuePlugin(),
      svgBuilder(["./src/assets/icons/"], base, outDir, "assets", NODE_ENV),
      bannerPlugin(`\n build based on ape-volo-admin \n Time : ${timestamp}`),
      vueFilePathPlugin("./src/plugins/pathInfo.json"),
    ],
  };
  return config;
};

🏗️ 核心架构设计

1. 路由系统

路由系统采用静态路由 + 动态路由混合模式,实现灵活的权限控制与路由管理。

1.1 静态路由配置

配置文件: src/router/index.js

静态路由用于定义无需权限即可访问的公共页面,如登录页、404 页等。

javascript
import { createRouter, createWebHashHistory } from "vue-router";

const routes = [
  {
    path: "/",
    redirect: "/login",
  },
  {
    path: "/login",
    name: "Login",
    component: () => import("@/views/login/index.vue"),
  },
  {
    path: "/:catchAll(.*)",
    meta: {
      closeTab: true,
    },
    component: () => import("@/views/error/index.vue"),
  },
];

const router = createRouter({
  history: createWebHashHistory(),
  routes,
});

export function setupRouter(app) {
  app.use(router);
}

export default router;

1.2 动态路由处理

配置文件: src/utils/asyncRouter.js

动态路由根据用户权限从后端获取菜单配置,自动生成路由表并动态注册。支持多层嵌套路由和 iframe 外部页面集成。

javascript
const viewModules = import.meta.glob("../views/**/*.vue");
import IframeView from "@/components/iframe/index.vue";

/**
 * 动态处理异步路由,递归处理 children
 * @param {Array} asyncRouter 路由配置数组
 */
export function dynamicRouterHandle(asyncRouter) {
  asyncRouter.forEach((route) => {
    // 1. iframe 类型
    if (route.meta && route.meta.iframeUrl) {
      route.component = IframeView;
    }
    if (typeof route.component === "string") {
      // 支持 component 写法如 "views/home/index.vue" 或 "home/index.vue"
      route.meta = route.meta || {};
      route.meta.path = "/src/" + route.component;
      route.component = dynamicImport(viewModules, route.component);
    }
    if (Array.isArray(route.children) && route.children.length) {
      dynamicRouterHandle(route.children);
    }
  });
}

/**
 * 动态匹配 import.meta.glob 导入的视图组件
 * @param {Object} dynamicViewsModules
 * @param {String} component 形如 'views/home/index.vue' 或 'home/index.vue'
 * @returns {Function|undefined}
 */
function dynamicImport(dynamicViewsModules, component) {
  let compPath = component;
  // 补全 views/ 前缀
  if (!/^views\//.test(compPath)) {
    compPath = "views/" + compPath;
  }
  // 补全 .vue 后缀
  if (!/\.vue$/.test(compPath)) {
    compPath += ".vue";
  }
  // 转 glob 匹配的相对路径
  const fullPath = `../${compPath}`;
  return dynamicViewsModules[fullPath];
}

2. 状态管理架构

采用 Pinia 作为状态管理方案,模块化设计,类型安全,支持 Vue DevTools 调试。

2.1 应用全局状态

配置文件: src/pinia/modules/app.js

管理应用的主题、布局、设备适配等全局配置,所有配置支持本地持久化存储。

javascript
/**
 * 应用程序全局状态管理 Store
 * @description 管理应用的主题、布局、设备类型等全局配置
 * @returns {Object} 应用状态和相关方法
 */
export const useAppStore = defineStore("app", () => {
  // 设备和界面状态
  const device = ref("");
  const drawerSize = ref("");
  const operateMinWith = ref("240");

  // 默认配置对象
  const defaultConfig = {
    weakness: false,
    grey: false,
    primaryColor: "#3b82f6",
    showTabs: true,
    darkMode: localStorage.getItem("vueuse-color-scheme") || "auto",
    layoutSideWidth: 256,
    layoutSideCollapsedWidth: 80,
    layoutSideItemHeight: 48,
    showWatermark: true,
    sideMode: MenuNavigation.SidebarNav,
    transitionType: "slide",
    cornerSize: BorderRadiusType.Small,
  };

  // 响应式配置对象
  const config = reactive({ ...defaultConfig });

  // 从本地存储加载用户偏好设置
  const sysPreferences = JSON.parse(
    localStorage.getItem("sysPreferences") || "{}"
  );
  if (sysPreferences) {
    Object.keys(config).forEach((key) => {
      if (sysPreferences[key] !== undefined) {
        config[key] = sysPreferences[key];
      }
    });
  }

  // 监听配置变化并保存到本地存储
  watch(
    config,
    (val) => {
      localStorage.setItem("sysPreferences", JSON.stringify(val));
    },
    { deep: true }
  );

  // 暗黑模式管理
  const isDark = useDark({
    selector: "html",
    attribute: "class",
    valueDark: "dark",
    valueLight: "light",
  });

  // 系统偏好的暗黑模式
  const preferredDark = usePreferredDark();

  /**
   * 切换主题模式
   * @param {boolean} darkMode - 是否为暗黑模式
   * @returns {void}
   */
  const toggleTheme = (darkMode) => {
    const sysPreferences = JSON.parse(
      localStorage.getItem("sysPreferences") || "{}"
    );
    if (darkMode) {
      sysPreferences.darkMode = "dark";
    } else {
      sysPreferences.darkMode = "light";
    }
    localStorage.setItem("sysPreferences", JSON.stringify(sysPreferences));
    isDark.value = darkMode;
  };

  /**
   * 切换弱视模式
   * @param {boolean} e - 是否启用弱视模式
   * @returns {void}
   */
  const toggleWeakness = (e) => {
    config.weakness = e;
  };

  /**
   * 切换灰度模式
   * @param {boolean} e - 是否启用灰度模式
   * @returns {void}
   */
  const toggleGrey = (e) => {
    config.grey = e;
  };

  /**
   * 切换主色调
   * @param {string} e - 主色调颜色值
   * @returns {void}
   */
  const togglePrimaryColor = (e) => {
    config.primaryColor = e;
  };

  /**
   * 切换标签页显示
   * @param {boolean} e - 是否显示标签页
   * @returns {void}
   */
  const toggleTabs = (e) => {
    config.showTabs = e;
  };

  /**
   * 切换设备类型
   * @param {string} e - 设备类型 ('mobile' | 'desktop')
   * @returns {void}
   */
  const toggleDevice = (e) => {
    if (e === "mobile") {
      drawerSize.value = "100%";
      operateMinWith.value = "60";
    } else {
      drawerSize.value = "800";
      operateMinWith.value = "80";
    }
    device.value = e;
  };

  /**
   * 切换暗黑模式配置
   * @param {string} e - 暗黑模式设置 ('auto' | 'dark' | 'light')
   * @returns {void}
   */
  const toggleDarkMode = (e) => {
    config.darkMode = e;
  };

  // 监听暗黑模式配置变化
  watchEffect(() => {
    localStorage.setItem("vueuse-color-scheme", config.darkMode);
    if (config.darkMode === "auto") {
      isDark.value = preferredDark.value;
      return;
    }
    isDark.value = config.darkMode === "dark";
  });

  /**
   * 切换侧边栏宽度
   * @param {number} e - 侧边栏宽度值
   * @returns {void}
   */
  const toggleConfigSideWidth = (e) => {
    config.layoutSideWidth = e;
  };

  /**
   * 切换侧边栏收缩宽度
   * @param {number} e - 侧边栏收缩宽度值
   * @returns {void}
   */
  const toggleConfigSideCollapsedWidth = (e) => {
    config.layoutSideCollapsedWidth = e;
  };

  /**
   * 切换侧边栏项目高度
   * @param {number} e - 侧边栏项目高度值
   * @returns {void}
   */
  const toggleConfigSideItemHeight = (e) => {
    config.layoutSideItemHeight = e;
  };

  /**
   * 切换水印显示
   * @param {boolean} e - 是否显示水印
   * @returns {void}
   */
  const toggleConfigWatermark = (e) => {
    config.showWatermark = e;
  };

  /**
   * 切换侧边栏模式
   * @param {string} e - 侧边栏模式
   * @returns {void}
   */
  const toggleSideMode = (e) => {
    config.sideMode = e;
  };

  /**
   * 切换过渡动画类型
   * @param {string} e - 过渡动画类型
   * @returns {void}
   */
  const toggleTransition = (e) => {
    config.transitionType = e;
  };

  /**
   * 切换圆角大小
   * @param {string} e - 圆角大小类型
   * @returns {void}
   */
  const toggleCornerSize = (e) => {
    config.cornerSize = e;
  };

  /**
   * 重置配置到默认值
   * @returns {void}
   */
  const resetConfig = () => {
    for (let baseCoinfgKey in defaultConfig) {
      config[baseCoinfgKey] = defaultConfig[baseCoinfgKey];
    }
  };

  // 监听弱视和灰度模式变化,应用到DOM
  watchEffect(() => {
    document.documentElement.classList.toggle("html-weakenss", config.weakness);
    document.documentElement.classList.toggle("html-grey", config.grey);
  });

  // 监听主色调变化,应用主题色
  watchEffect(() => {
    setBodyPrimaryColor(config.primaryColor, isDark.value ? "dark" : "light");
  });

  // 监听圆角大小变化,应用圆角样式
  watchEffect(() => {
    setCornerSize(config.cornerSize);
  });
});

2.2 用户状态管理

配置文件: src/pinia/modules/user.js

管理用户登录状态、个人信息、权限信息等,提供登录、登出、令牌刷新等核心方法。

javascript
/**
 * 用户状态管理 Store
 * @description 管理用户信息、登录状态和相关操作
 * @returns {Object} 用户状态和相关方法
 */
export const useUserStore = defineStore("user", () => {
  // 加载状态实例
  const loadingInstance = ref(null);

  // 用户信息对象
  const userInfo = ref({
    id: 0,
    userName: "",
    nickName: "",
    headerImg: "",
    authority: {},
    genderCode: 0,
    email: "",
    phone: "",
    dept: {},
    roleCodes: [],
    authCodes: [],
  });

  /**
   * 设置用户信息
   * @param {Object} user - 用户基本信息
   * @param {Array} roleCodes - 角色编码数组
   * @param {Array} authCodes - 权限编码数组
   * @returns {void}
   */
  const setUserInfo = (user, roleCodes, authCodes) => {
    userInfo.value = {
      ...userInfo.value,
      ...user,
      roleCodes,
      authCodes,
    };
  };

  /**
   * 重置用户信息
   * @param {Object} value - 要更新的用户信息字段,默认为空对象
   * @returns {void}
   */
  const ResetUserInfo = (value = {}) => {
    userInfo.value = {
      ...userInfo.value,
      ...value,
    };
  };

  /**
   * 获取用户信息
   * @returns {Promise<Object>} 返回用户信息的Promise对象
   */
  const GetUserInfo = async () => {
    const res = await getUserInfo();
    setUserInfo(res.data.user, res.data.roleCodes, res.data.authCodes);
    return res;
  };

  /**
   * 系统登录
   * @param {string} username - 用户名
   * @param {string} password - 密码
   * @param {string} captcha - 验证码
   * @param {string} captchaId - 验证码ID
   * @returns {Promise<boolean>} 登录成功返回true,失败返回false
   */
  const LoginIn = async (username, password, captcha, captchaId) => {
    try {
      // 显示全屏加载状态
      loadingInstance.value = ElLoading.service({
        fullscreen: true,
        text: "登录中,请稍候...",
      });

      // 调用登录API
      const res = await login(username, password, captcha, captchaId);

      // 设置用户信息和权限
      setUserInfo(res.data.user, res.data.roleCodes, res.data.authCodes);
      setApeToken(res.data.token);

      // 初始化路由信息
      const routerStore = useRouterStore();
      await routerStore.SetAsyncRouter();
      const asyncRouters = routerStore.asyncRouters;

      // 注册到路由表里
      asyncRouters.forEach((asyncRouter) => {
        router.addRoute(asyncRouter);
      });

      // 处理重定向或默认页面跳转
      if (router.currentRoute.value.query.redirect) {
        await router.replace(router.currentRoute.value.query.redirect);
        return true;
      }
      const defaultRouter = "dashboard";
      if (!router.hasRoute(defaultRouter)) {
        ElMessage.error("请联系管理员进行授权");
      } else {
        await router.replace({ name: defaultRouter });
      }

      // 检测操作系统类型并保存
      const isWindows = /windows/i.test(navigator.userAgent);
      window.localStorage.setItem("osType", isWindows ? "WIN" : "MAC");

      // 显示登录成功通知
      ElNotification({
        title: "登录成功",
        message: "欢迎回来:" + username,
        type: "success",
      });
      return true;
    } catch (error) {
      console.error("LoginIn error:", error);
      return false;
    } finally {
      // 关闭加载状态
      loadingInstance.value?.close();
    }
  };

  /**
   * 用户登出
   * @returns {Promise<void>} 无返回值的Promise
   */
  const LoginOut = async () => {
    // 调用登出API
    await logout();
    // 清除本地存储
    await ClearStorage();
    // 跳转到登录页面
    router.push({ name: "Login", replace: true });
    // 刷新页面
    window.location.reload();
  };

  /**
   * 清除本地存储数据
   * @returns {Promise<void>} 无返回值的Promise
   */
  const ClearStorage = async () => {
    // 清除本地存储
    localStorage.clear();
    // 清除会话存储
    sessionStorage.clear();
  };
});

2.3 字典数据管理

配置文件: src/pinia/modules/dictionary.js

统一管理系统字典数据,提供智能缓存机制,避免重复请求。

javascript
import { single } from "@/api/system/dictionary";

import { defineStore } from "pinia";
import { ref } from "vue";

/**
 * 字典数据状态管理 Store
 * @description 管理系统字典数据的缓存和获取
 * @returns {Object} 字典状态和相关方法
 */
export const useDictionaryStore = defineStore("dictionary", () => {
  // 字典数据映射表
  const dictionaryMap = ref({});

  /**
   * 设置字典数据到映射表
   * @param {Object} dictionaryRes - 字典数据对象
   * @returns {void}
   */
  const setDictionaryMap = (dictionaryRes) => {
    dictionaryMap.value = { ...dictionaryMap.value, ...dictionaryRes };
  };

  /**
   * 获取指定名称的字典数据
   * @param {string} name - 字典名称
   * @returns {Promise<Array>} 返回字典项数组的Promise
   * @description 优先从缓存获取,缓存不存在则从API获取并缓存
   */
  const getDictionary = async (name) => {
    // 检查缓存中是否已存在该字典数据
    if (dictionaryMap.value[name] && dictionaryMap.value[name].length) {
      return dictionaryMap.value[name];
    } else {
      // 从API获取字典数据
      const res = await single({ name });
      const dictionaryRes = {};
      const dict = [];
      // 处理字典详情数据
      res.data.dictDetails &&
        res.data.dictDetails.forEach((item) => {
          dict.push({
            label: item.label,
            value: item.value,
          });
        });
      // 将数据加入缓存
      dictionaryRes[res.data.name] = dict;
      setDictionaryMap(dictionaryRes);
      return dictionaryMap.value[name];
    }
  };

  return {
    dictionaryMap,
    setDictionaryMap,
    getDictionary,
  };
});

2.4 路由状态管理

配置文件: src/pinia/modules/router.js

管理动态路由、菜单导航、路由缓存(keep-alive)等功能。

javascript
// 非布局路由数组
const notLayoutRouterArr = [];
// 需要缓存的路由数组
const keepAliveRoutersArr = [];
// 路由名称映射表
const nameMap = {};

/**
 * 格式化路由数据
 * @param {Array} routes - 路由数组
 * @param {Object} routeMap - 路由映射对象
 * @param {Object} parent - 父级路由
 * @returns {void}
 */
const formatRouter = (routes, routeMap, parent) => {
  routes &&
    routes.forEach((item) => {
      // 设置父级路由引用
      item.parent = parent;
      // 设置隐藏状态
      item.meta.hidden = item.hidden;
      // 处理内部链接类型菜单
      if (item.menuType === MenuType.InternalLink) {
        item.meta.iframeUrl = item.component;
      }
      // 处理外部链接类型菜单
      if (item.menuType === MenuType.ExternalLink) {
        item.name = item.component;
      }
      // 添加到路由映射表
      routeMap[item.name] = item;
      // 递归处理子路由
      if (item.children && item.children.length > 0) {
        formatRouter(item.children, routeMap, item);
      }
    });
};

/**
 * 过滤需要缓存的路由
 * @param {Array} routes - 路由数组
 * @returns {void}
 */
const KeepAliveFilter = (routes) => {
  routes &&
    routes.forEach((item) => {
      // 子菜单中有 keep-alive 的,父菜单也必须 keep-alive,否则无效。这里将子菜单中有 keep-alive 的父菜单也加入。
      if (
        (item.children && item.children.some((ch) => ch.meta.keepAlive)) ||
        item.meta.keepAlive
      ) {
        const path = item.meta.path;
        keepAliveRoutersArr.push(pathInfo[path]);
        nameMap[item.name] = pathInfo[path];
      }
      // 递归处理子路由
      if (item.children && item.children.length > 0) {
        KeepAliveFilter(item.children);
      }
    });
};

/**
 * 路由状态管理 Store
 * @description 管理动态路由、菜单导航和路由缓存
 * @returns {Object} 路由状态和相关方法
 */
export const useRouterStore = defineStore("router", () => {
  // 需要缓存的路由组件数组
  const keepAliveRouters = ref([]);
  // 异步路由标志,用于触发路由更新
  const asyncRouterFlag = ref(0);

  /**
   * 设置需要缓存的路由
   * @param {Array} history - 路由历史记录
   * @returns {void}
   */
  const setKeepAliveRouters = (history) => {
    const keepArrTemp = [];
    history.forEach((item) => {
      if (nameMap[item.name]) {
        keepArrTemp.push(nameMap[item.name]);
      }
    });
    // 去重并更新缓存路由数组
    keepAliveRouters.value = Array.from(new Set(keepArrTemp));
  };

  const route = useRoute();

  // 监听设置缓存事件
  emitter.on("setKeepAlive", setKeepAliveRouters);

  // 异步路由数组
  const asyncRouters = ref([]);

  // 顶部菜单数组
  const topMenu = ref([]);

  // 左侧菜单数组
  const leftMenu = ref([]);

  // 菜单映射对象
  const menuMap = {};

  // 当前激活的顶部菜单
  const topActive = ref("");

  /**
   * 设置左侧菜单
   * @param {string} name - 顶部菜单名称
   * @returns {Array} 左侧菜单数组
   */
  const setLeftMenu = (name) => {
    // 保存到会话存储
    sessionStorage.setItem("topActive", name);
    topActive.value = name;
    leftMenu.value = [];
    // 设置对应的左侧菜单
    if (menuMap[name]?.children) {
      leftMenu.value = menuMap[name].children;
    }
    return menuMap[name]?.children;
  };

  /**
   * 根据路由名称查找对应的顶部菜单
   * @param {Object} menuMap - 菜单映射对象
   * @param {string} routeName - 路由名称
   * @returns {string|null} 顶部菜单名称
   */
  const findTopActive = (menuMap, routeName) => {
    for (let topName in menuMap) {
      const topItem = menuMap[topName];
      // 检查直接子菜单
      if (topItem.children?.some((item) => item.name === routeName)) {
        return topName;
      }
      // 递归检查深层子菜单
      const foundName = findTopActive(topItem.children || {}, routeName);
      if (foundName) {
        return topName;
      }
    }
    return null;
  };

  // 监听路由变化,自动设置菜单
  watchEffect(() => {
    let topActive = sessionStorage.getItem("topActive");
    // 初始化菜单内容,防止重复添加
    topMenu.value = [];
    asyncRouters.value[0]?.children.forEach((item) => {
      if (item.hidden) return;
      menuMap[item.name] = item;
      topMenu.value.push({ ...item, children: [] });
    });
    // 如果没有激活的顶部菜单,自动查找
    if (!topActive || topActive === "undefined" || topActive === "null") {
      topActive = findTopActive(menuMap, route.name);
    }
    setLeftMenu(topActive);
  });

  // 路由映射对象
  const routeMap = {};

  /**
   * 从后台获取并设置动态路由
   * @returns {Promise<boolean>} 设置成功返回true
   */
  const SetAsyncRouter = async () => {
    // 增加异步路由标志,触发相关组件更新
    asyncRouterFlag.value++;

    // 基础路由结构
    const baseRouter = [
      {
        path: "/layout",
        name: "layout",
        component: "views/layout/index.vue",
        meta: {
          title: "底层layout",
        },
        children: [],
      },
    ];

    // 从后台获取菜单数据
    const resMenu = await build();
    const asyncRouter = resMenu.data;

    // 添加重载页面路由
    asyncRouter &&
      asyncRouter.push({
        path: "reload",
        name: "Reload",
        hidden: true,
        meta: {
          title: "",
          closeTab: true,
        },
        component: "views/error/reload.vue",
      });

    // 格式化路由数据
    formatRouter(asyncRouter, routeMap);
    baseRouter[0].children = asyncRouter;

    // 添加非布局路由
    if (notLayoutRouterArr.length !== 0) {
      baseRouter.push(...notLayoutRouterArr);
    }

    // 处理动态路由
    dynamicRouterHandle(baseRouter);

    // 过滤缓存路由
    KeepAliveFilter(asyncRouter);

    // 保存路由数据
    asyncRouters.value = baseRouter;
    return true;
  };
});

3. HTTP 请求封装

配置文件: src/utils/request.js

基于 Axios 封装的 HTTP 请求工具,提供请求/响应拦截、自动令牌管理、智能加载状态、错误处理等企业级功能。

核心特性:

  • ✅ 自动携带认证令牌
  • ✅ 令牌过期自动刷新
  • ✅ 智能全局加载状态
  • ✅ 统一错误处理
  • ✅ 请求队列管理
javascript
/**
 * Axios 实例配置
 * @description 创建预配置的 Axios 实例,设置基础 URL 和超时时间
 */
const service = axios.create({
  baseURL: import.meta.env.VITE_BASE_API, // 从环境变量获取 API 基础地址
  timeout: setting.timeout, // 从配置文件获取请求超时时间
});

// 加载状态管理变量
let activeAxios = 0; // 活跃的请求数量计数器
let timer; // 加载延迟定时器
let loadingInstance; // Element Plus 加载实例

/**
 * 显示全局加载状态
 * @function showLoading
 * @description 智能管理全局加载状态,避免频繁的加载状态切换
 *
 * 设计理念:
 * - 使用计数器管理并发请求
 * - 延迟显示加载状态,避免闪烁
 * - 支持自定义加载目标元素
 *
 * @param {Object} [option={}] - 加载配置选项
 * @param {HTMLElement|null} [option.target=null] - 加载遮罩的目标元素,默认为基础加载容器
 *
 * @example
 * // 默认全屏加载
 * showLoading()
 *
 * // 指定容器加载
 * showLoading({ target: document.getElementById('my-container') })
 */
const showLoading = (
  option = {
    target: null,
  }
) => {
  const loadDom = document.getElementById("base-load-dom");
  activeAxios++; // 增加活跃请求计数

  // 清除之前的定时器,避免重复创建加载实例
  if (timer) {
    clearTimeout(timer);
  }

  // 延迟 400ms 显示加载状态,避免快速请求的加载闪烁
  timer = setTimeout(() => {
    if (activeAxios > 0) {
      if (!option.target) option.target = loadDom;
      loadingInstance = ElLoading.service(option);
    }
  }, 400);
};

/**
 * 关闭全局加载状态
 * @function closeLoading
 * @description 智能管理加载状态的关闭,确保所有请求完成后才关闭加载
 *
 * 设计理念:
 * - 递减请求计数器
 * - 只有当所有请求完成时才关闭加载
 * - 清理定时器,防止内存泄漏
 */
const closeLoading = () => {
  activeAxios--; // 减少活跃请求计数

  // 当所有请求都完成时,关闭加载状态
  if (activeAxios <= 0) {
    clearTimeout(timer);
    loadingInstance && loadingInstance.close();
  }
};

/**
 * 请求拦截器
 * @description 在发送请求前进行统一处理,添加认证头、加载状态等
 */
service.interceptors.request.use(
  (config) => {
    // 根据配置决定是否显示加载状态
    if (!config.donNotShowLoading) {
      showLoading(config.loadingOption);
    }

    // 获取当前存储的认证令牌
    var apeToken = getApeToken();
    if (apeToken != null) {
      const nowTime = Date.now();
      const expireTime = apeToken.expires;

      // 检查令牌是否未过期,如果有效则添加到请求头
      if (nowTime < expireTime) {
        config.headers[
          "Authorization"
        ] = `${apeToken.tokenType} ${apeToken.accessToken}`;
      }
    }

    // 设置默认的请求内容类型
    config.headers["Content-Type"] = "application/json";
    return config;
  },
  (error) => {
    // 请求错误时关闭加载状态并显示错误信息
    if (!error.config.donNotShowLoading) {
      closeLoading();
    }
    ElMessage({
      showClose: true,
      message: error,
      type: "error",
    });
    console.error("Request interceptor error:", error);
    return Promise.reject(error);
  }
);

/**
 * 响应拦截器
 * @description 统一处理响应结果,包括成功响应和错误处理
 */
service.interceptors.response.use(
  (response) => {
    // 成功响应时关闭加载状态
    if (!response.config.donNotShowLoading) {
      closeLoading();
    }
    return response;
  },
  (error) => {
    // 错误响应时关闭加载状态
    if (!error.config.donNotShowLoading) {
      closeLoading();
    }

    // 处理网络错误(无响应对象)
    if (!error.response) {
      ElMessage.error(error.response.data.message || "网络错误,请稍后再试");
      return Promise.reject(error);
    }

    let message = "";

    // HTTP 状态码对应的错误信息映射
    const statusMessageMap = {
      400: "请求错误",
      500: "服务器内部错误",
      404: "请求的资源未找到",
      403: "没有权限访问此资源",
    };

    /**
     * 处理 Blob 类型的错误响应
     * @description 当下载文件等操作出错时,服务器可能返回 JSON 格式的错误信息包装在 Blob 中
     */
    if (
      error.response.data instanceof Blob &&
      error.response.data.type.toLowerCase().indexOf("json") !== -1
    ) {
      const reader = new FileReader();
      reader.readAsText(error.response.data, "utf-8");
      reader.onload = function () {
        message = JSON.parse(reader.result).message;
        ElMessage.error(message || statusMessageMap[status] || "请求失败");
      };
    } else {
      const status = error.response.status;
      message = error.response.data.message;

      // 401 未授权状态特殊处理 - 尝试刷新令牌
      if (status === 401) {
        return handleTokenRefresh(error.config);
      }

      // 显示对应的错误信息
      ElMessage.error(message || statusMessageMap[status] || "请求失败");
    }
    return Promise.reject(error);
  }
);

export default service;

/**
 * 令牌刷新状态管理
 */
let isRefreshing = false; // 是否正在刷新令牌的标志
let failedQueue = []; // 令牌刷新期间失败请求的重试队列

/**
 * 处理令牌刷新逻辑
 * @function handleTokenRefresh
 * @description 当遇到 401 未授权错误时,自动尝试刷新令牌并重试失败的请求
 *
 * 核心机制:
 * - 防止并发刷新:使用 isRefreshing 标志确保同时只有一个刷新操作
 * - 请求队列:将刷新期间的失败请求加入队列,刷新成功后批量重试
 * - 令牌过期检查:验证刷新令牌是否仍然有效
 * - 自动登出:刷新失败时清除令牌并重定向到登录页
 *
 * @param {Object} config - 失败请求的原始配置对象
 * @returns {Promise} 返回重试请求的 Promise
 *
 * @example
 * // 该函数通常由响应拦截器自动调用,无需手动调用
 * // 当 API 返回 401 状态时自动触发令牌刷新流程
 */
export const handleTokenRefresh = async (config) => {
  return new Promise((resolve) => {
    /**
     * 重试请求函数
     * @description 使用最新的令牌重新发送原始请求
     */
    const retryRequest = () => {
      var latestToken = getApeToken();
      config.headers.Authorization = `${latestToken.tokenType} ${latestToken.accessToken}`;
      resolve(service(config));
    };

    // 将重试函数加入队列
    failedQueue.push(retryRequest);

    // 如果没有正在进行的刷新操作,则开始刷新流程
    if (!isRefreshing) {
      isRefreshing = true;
      var oldToken = getApeToken();
      const nowTime = Date.now();
      const refreshTokenExpires = oldToken.refreshTokenExpires;

      // 检查刷新令牌是否已过期
      if (nowTime >= refreshTokenExpires) {
        ElNotification({
          title: "提示",
          message: "您的会话已过期,请重新登录",
          type: "info",
        });
        clearToken();
        router.push("/login");
        return;
      }
      // 调用刷新令牌 API
      refreshToken({ token: oldToken.accessToken })
        .then((res) => {
          // 刷新成功:保存新令牌并重试所有失败的请求
          setApeToken(res.data);
          failedQueue.forEach((callback) => callback());
          failedQueue.length = 0; // 清空重试队列
        })
        .catch(async () => {
          // 刷新失败:清除令牌并重定向到登录页
          ElNotification({
            title: "提示",
            message: "您的会话已过期,请重新登录",
            type: "info",
          });
          await clearToken();
          router.push("/login");
        })
        .finally(() => {
          // 无论成功失败都重置刷新状态
          isRefreshing = false;
        });
    }
  });
};

🔐 权限控制系统

自定义权限指令

配置文件: src/directive/permission/index.js

提供细粒度的权限控制指令,支持在模板中直接进行权限判断,无权限元素自动隐藏。

1 角色权限指令 v-has-role

用于根据用户角色控制元素显示,支持单个或多个角色的判断。

javascript
export const hasRole = {
  /**
   * 元素挂载时的权限检查
   * @param {HTMLElement} el - 要控制的DOM元素
   * @param {Object} binding - Vue指令绑定对象
   * @param {string|string[]} binding.value - 需要的角色标识,支持字符串或数组
   * @throws {Error} 当角色参数格式不正确时抛出错误
   */
  mounted(el, binding) {
    const requiredRoles = binding.value;

    // 参数验证:确保提供了有效的角色标识
    if (
      !requiredRoles ||
      (typeof requiredRoles !== "string" && !Array.isArray(requiredRoles))
    ) {
      throw new Error(
        "v-has-role 指令需要提供有效的角色标识参数。\n" +
          "支持格式:\n" +
          "• 单个角色:v-has-role=\"'admin'\"\n" +
          "• 多个角色:v-has-role=\"['admin', 'manager', 'guest']\"\n" +
          '• 变量绑定:v-has-role="roleVariable"'
      );
    }

    const userStore = useUserStore();

    // 权限验证逻辑
    const hasAuth = Array.isArray(requiredRoles)
      ? // 数组模式:检查用户是否拥有任一指定角色
        requiredRoles.some((role) =>
          userStore.userInfo.roleCodes.includes(role)
        )
      : // 字符串模式:检查用户是否拥有指定角色
        userStore.userInfo.roleCodes.includes(requiredRoles);

    // 无权限时移除DOM元素
    if (!hasAuth && el.parentNode) {
      el.parentNode.removeChild(el);
    }
  },
};

3.2 操作权限指令 v-has-perm

用于根据用户权限码控制元素显示,实现细粒度的按钮级权限控制。

javascript
export const hasPerm = {
  /**
   * 元素挂载时的权限检查
   * @param {HTMLElement} el - 要控制的DOM元素
   * @param {Object} binding - Vue指令绑定对象
   * @param {string|string[]} binding.value - 需要的权限标识,支持字符串或数组
   * @throws {Error} 当权限参数格式不正确时抛出错误
   */
  mounted(el, binding) {
    const requiredPerms = binding.value;

    // 参数验证:确保提供了有效的权限标识
    if (
      !requiredPerms ||
      (typeof requiredPerms !== "string" && !Array.isArray(requiredPerms))
    ) {
      throw new Error(
        "v-has-perm 指令需要提供有效的权限标识参数。\n" +
          "支持格式:\n" +
          "• 单个权限:v-has-perm=\"'sys:user:add'\"\n" +
          "• 多个权限:v-has-perm=\"['sys:user:add', 'sys:user:edit', 'sys:user:delete']\"\n" +
          '• 变量绑定:v-has-perm="permissionVariable"\n' +
          "权限格式规范:模块:功能:操作 (如:sys:user:add)"
      );
    }

    const userStore = useUserStore();

    // 权限验证逻辑
    const hasAuth = Array.isArray(requiredPerms)
      ? // 数组模式:检查用户是否拥有任一指定权限
        requiredPerms.some((perm) =>
          userStore.userInfo.authCodes.includes(perm)
        )
      : // 字符串模式:检查用户是否拥有指定权限
        userStore.userInfo.authCodes.includes(requiredPerms);

    // 无权限时移除DOM元素
    if (!hasAuth && el.parentNode) {
      el.parentNode.removeChild(el);
    }
  },
};

🎨 主题定制

Element Plus 主题样式定制

配置文件: src/style/element/index.scss

通过 SCSS 变量覆盖 Element Plus 默认主题色,实现品牌化的视觉定制。

配置示例:

scss
// 使用 SCSS @forward 规则覆盖 Element Plus 默认主题变量
@forward "element-plus/theme-chalk/src/common/var.scss" with (
  $colors: (
    "primary": (
      "base": #409eff,
      // 主色调 - 用于主要操作按钮、链接等
    ),
    "success": (
      "base": #67c23a,
      // 成功色 - 用于成功提示、状态指示
    ),
    "warning": (
      "base": #e6a23c,
      // 警告色 - 用于警告提示
    ),
    "danger": (
      "base": #f56c6c,
      // 危险色 - 用于删除操作、错误提示
    ),
    "error": (
      "base": #f56c6c,
      // 错误色 - 用于表单验证错误
    ),
    "info": (
      "base": #909399,
      // 信息色 - 用于一般信息提示
    ),
  )
);

自定义主题色:src/pinia/modules/app.js 中可动态切换主题色,支持用户个性化配置。

🐛 常见问题与解决方案

Q1: 路由懒加载组件失败,页面显示空白?

原因分析:

  • 组件文件路径错误或文件不存在
  • 动态导入语法错误
  • Vite 的 glob 匹配模式不正确

解决方案:

javascript
// ✅ 正确写法
const viewModules = import.meta.glob("../views/**/*.vue");

// ❌ 错误写法
const viewModules = import.meta.glob("/views/**/*.vue"); // 不要使用绝对路径

Q2: Element Plus 组件样式不生效或显示异常?

原因分析:

  • 样式文件未正确导入
  • CSS 优先级冲突
  • Scoped 作用域限制

解决方案:

javascript
// main.js 中确保导入 Element Plus 样式
import "element-plus/dist/index.css";
import "./style/element/index.scss"; // 自定义主题样式

Q3: Pinia 状态在页面刷新后丢失?

原因分析:

  • Pinia 状态默认存储在内存中,刷新页面会重置

解决方案:

javascript
// 方案1:使用 localStorage 持久化
watch(
  config,
  (val) => {
    localStorage.setItem("sysPreferences", JSON.stringify(val));
  },
  { deep: true }
);

// 方案2:使用 pinia-plugin-persistedstate 插件
import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";

const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);

Q4: 生产环境打包后静态资源 404?

原因分析:

  • Vite 配置的 base 路径与实际部署路径不一致

解决方案:

javascript
// vite.config.js
export default {
  base: "/", // 根路径部署
  // 或
  base: "/my-app/", // 子路径部署
};

Q5: 开发环境 API 跨域问题?

原因分析:

  • 前后端端口不一致导致的浏览器同源策略限制

解决方案:

javascript
// vite.config.js 配置代理
server: {
  proxy: {
    '/api': {
      target: 'http://localhost:8080',
      changeOrigin: true,
      rewrite: (path) => path.replace(/^\/api/, '')
    }
  }
}

📚 参考资源

官方文档

技术官方文档说明
Vue 3https://vuejs.org/Vue.js 核心框架文档
Vitehttps://vitejs.dev/Vite 构建工具文档
Element Plushttps://element-plus.org/Element Plus 组件库文档
Piniahttps://pinia.vuejs.org/Pinia 状态管理文档
Vue Routerhttps://router.vuejs.org/Vue Router 路由文档

推荐学习资源

社区资源

版权所有 © 2021-2026 ApeVolo-Team