🎨 前端技术架构
Ape-Volo-Admin 前端采用业界领先的技术栈构建,基于 Vue 3 渐进式框架,结合 Vite 6 极速构建工具与 Element Plus 企业级组件库,为开发者提供现代化、高效能的开发体验,为用户呈现流畅优雅的交互界面。
🚀 核心技术栈
核心框架
| 技术 | 版本 | 说明 |
|---|---|---|
| Vue 3 | 3.x | 渐进式 JavaScript 框架,采用 Composition API 提供更灵活的代码组织方式 |
| Vite 6 | 6.x | 下一代前端构建工具,利用原生 ES 模块实现极速冷启动与热更新 |
| Element Plus | 2.x | 基于 Vue 3 的企业级 UI 组件库,提供丰富的高质量组件 |
状态与路由
| 技术 | 说明 |
|---|---|
| Pinia | Vue 3 官方推荐的新一代状态管理方案,轻量级且类型安全 |
| Vue Router 4 | Vue.js 官方路由管理器,支持动态路由和权限控制 |
开发工具链
| 工具 | 用途 |
|---|---|
| TypeScript | JavaScript 超集,提供静态类型检查(可选) |
| ESLint | 代码质量与规范检查工具 |
| Prettier | 代码格式化工具,统一代码风格 |
| Sass/SCSS | CSS 预处理器,增强样式编写能力 |
性能优化特性
- 插件生态 - 丰富的 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.0 | JavaScript 运行时环境 |
| 包管理器 | npm >= 8.0.0 或 yarn >= 1.22.0 | 依赖包管理工具 |
| Git | 最新稳定版 | 版本控制系统 |
快速开始
1. 安装依赖
# 进入前端项目目录
cd ape-volo-web
# 使用 npm 安装项目依赖
npm install
# 或使用 yarn(推荐,速度更快)
yarn install2. 常用命令
# 🚀 启动开发服务器(支持热更新)
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 构建工具的核心,定义了开发服务器、构建优化、插件等关键配置。
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 页等。
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 外部页面集成。
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
管理应用的主题、布局、设备适配等全局配置,所有配置支持本地持久化存储。
/**
* 应用程序全局状态管理 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
管理用户登录状态、个人信息、权限信息等,提供登录、登出、令牌刷新等核心方法。
/**
* 用户状态管理 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
统一管理系统字典数据,提供智能缓存机制,避免重复请求。
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)等功能。
// 非布局路由数组
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 请求工具,提供请求/响应拦截、自动令牌管理、智能加载状态、错误处理等企业级功能。
核心特性:
- ✅ 自动携带认证令牌
- ✅ 令牌过期自动刷新
- ✅ 智能全局加载状态
- ✅ 统一错误处理
- ✅ 请求队列管理
/**
* 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
用于根据用户角色控制元素显示,支持单个或多个角色的判断。
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
用于根据用户权限码控制元素显示,实现细粒度的按钮级权限控制。
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 @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 匹配模式不正确
解决方案:
// ✅ 正确写法
const viewModules = import.meta.glob("../views/**/*.vue");
// ❌ 错误写法
const viewModules = import.meta.glob("/views/**/*.vue"); // 不要使用绝对路径Q2: Element Plus 组件样式不生效或显示异常?
原因分析:
- 样式文件未正确导入
- CSS 优先级冲突
- Scoped 作用域限制
解决方案:
// main.js 中确保导入 Element Plus 样式
import "element-plus/dist/index.css";
import "./style/element/index.scss"; // 自定义主题样式Q3: Pinia 状态在页面刷新后丢失?
原因分析:
- Pinia 状态默认存储在内存中,刷新页面会重置
解决方案:
// 方案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 路径与实际部署路径不一致
解决方案:
// vite.config.js
export default {
base: "/", // 根路径部署
// 或
base: "/my-app/", // 子路径部署
};Q5: 开发环境 API 跨域问题?
原因分析:
- 前后端端口不一致导致的浏览器同源策略限制
解决方案:
// vite.config.js 配置代理
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}📚 参考资源
官方文档
| 技术 | 官方文档 | 说明 |
|---|---|---|
| Vue 3 | https://vuejs.org/ | Vue.js 核心框架文档 |
| Vite | https://vitejs.dev/ | Vite 构建工具文档 |
| Element Plus | https://element-plus.org/ | Element Plus 组件库文档 |
| Pinia | https://pinia.vuejs.org/ | Pinia 状态管理文档 |
| Vue Router | https://router.vuejs.org/ | Vue Router 路由文档 |
推荐学习资源
- Vue 3 Composition API - 官方 RFC
- Vite 插件开发 - 插件 API 文档
- TypeScript 集成 - Vue with TypeScript
社区资源
- Awesome Vue - 精选 Vue 资源列表
- Element Plus 主题编辑器 - 在线主题定制

