Vue 的前端框架 M-Admin 的学习与整理
本文基于源码与官方文档整理,帮助快速理解目录结构、运行机制、权限模型与二次开发方式。参考资料:M-Admin 官方文档
一、项目简介
- 定位:开源、免费、可商用的中后台前端脚手架,内置丰富组件与业务 Hooks,支持服务端菜单与按钮权限。
- 技术栈:Vue3 + Vite + TypeScript + Pinia + Vue-Router + Arco Design Vue + ECharts + Axios。
-
亮点特性:
- 模块化路由与服务端菜单(可开关)
- 基于
meta.roles与后端menuIds/operationIds的权限模型 - Axios 二次封装:请求/响应拦截、错误统一、重试、Token 注入
- 全局主题与布局配置:暗黑模式、TabBar、移动端适配
- 多语言内置:
zh-***与en-US
二、快速开始
- 环境要求
- Node.js >= 18.12
- 推荐包管理器:pnpm
- 安装与启动
pnpm install
pnpm dev
- 生产构建
pnpm build:prod
三、目录结构总览(关键路径)
-
src/main.ts:应用入口,注册 UI、路由、状态、指令、全局组件、i18n -
src/router/:路由与守卫-
routes/modules/*.ts:模块化路由 -
guard/:登录信息与权限守卫 -
constants.ts:白名单、默认路由常量
-
-
src/store/:Pinia 状态管理(app、user、tab-bar、table等) -
src/utils/request/:Axios 二次封装(实例、拦截器、类型、工具) -
src/***ponents/:全局业务组件(如MTable、MForm、MDict、MChart) -
src/config/settings.json:应用级配置(主题、菜单来源、布局等) -
src/locale/:国际化配置 -
src/views/:页面视图
四、启动与全局注册
// src/main.ts(节选)
import { createApp } from 'vue'
import ArcoVue, { Modal } from '@arco-design/web-vue'
import ArcoVueIcon from '@arco-design/web-vue/es/icon'
import global***ponents from '@/***ponents/index'
import Particles from 'particles.vue3'
import router from './router'
import store from './store'
import i18n from './locale'
import directive from './directive'
import App from './App.vue'
const app = createApp(App)
Modal._context = app._context
app.use(ArcoVue)
app.use(ArcoVueIcon)
app.use(router)
app.use(store)
app.use(i18n)
app.use(global***ponents)
app.use(directive)
app.use(Particles)
app.mount('#app')
要点:自动化注册 UI 库、全局组件、路由、状态、指令与多语言,开箱即用。
五、路由体系:模块化与守卫链
- 定义与创建路由(默认
/->login,注册 404/重定向页面)
// src/router/index.ts(节选)
import { createRouter, createWebHistory } from 'vue-router'
import { appRoutes } from './routes'
import { REDIRECT_MAIN, NOT_FOUND_ROUTE } from './routes/base'
import createRouteGuard from './guard'
const router = createRouter({
history: createWebHistory(''),
routes: [
{ path: '/', redirect: 'login' },
{ path: '/login', name: 'login', ***ponent: () => import('@/views/login/index.vue'), meta: { requiresAuth: false } },
...appRoutes,
REDIRECT_MAIN,
NOT_FOUND_ROUTE,
],
})
createRouteGuard(router)
export default router
- 守卫链:页面事件 -> 登录信息 -> 权限校验
// src/router/guard/index.ts
export default function createRouteGuard(router: Router) {
setupPageGuard(router)
setupUserLoginInfoGuard(router)
setupPermissionGuard(router)
}
- 登录信息守卫:未登录强制跳转登录
// src/router/guard/userLoginInfo.ts(节选)
if (isLogin()) {
next()
} else {
if (to.name === 'login') next()
else next({ name: 'login' })
}
- 权限守卫:基于
meta.roles与服务端菜单模式
// src/router/guard/permission.ts(节选)
const permissionsAllow = Permission.a***essRouter(to)
if (appStore.menuFromServer) {
if (!appStore.appAsyncMenus.length && !WHITE_LIST.find((el) => el.name === to.name)) {
await appStore.fetchServerMenuConfig()
}
if (permissionsAllow) next()
else next(NOT_FOUND)
} else {
if (permissionsAllow) next()
else {
const destination = Permission.findFirstPermissionRoute(appRoutes, userStore.role) || NOT_FOUND
next(destination)
}
}
- 常量与默认路由
// src/router/constants.ts(节选)
export const WHITE_LIST = [{ name: 'notFound', children: [] }, { name: 'login', children: [] }]
export const NOT_FOUND = { name: 'notFound' }
export const DEFAULT_ROUTE_NAME = 'Wel***e'
export const DEFAULT_ROUTE = { title: 'menu.dashboard.wel***e', name: DEFAULT_ROUTE_NAME, fullPath: '/dashboard/wel***e' }
- 模块化路由自动收集
// src/router/routes/index.ts(节选)
const modules = import.meta.glob('./modules/*.ts', { eager: true })
export const appRoutes = formatModules(modules, [])
- 路由元信息(示例):
id、locale、icon、requiresAuth、roles、order、level
// src/router/routes/modules/dashboard.ts(节选)
{
path: '/dashboard',
name: 'dashboard',
***ponent: DEFAULT_LAYOUT,
meta: { id: '12', locale: 'menu.home', icon: 'home', requiresAuth: true, order: 0 },
children: [
{
path: 'wel***e',
name: 'Wel***e',
***ponent: () => import('@/views/dashboard/wel***e/index.vue'),
meta: { locale: 'menu.dashboard.wel***e', requiresAuth: true, roles: ['*'], id: '1201' },
},
],
}
六、权限模型:角色与服务端菜单
- 角色路由判定
// src/hooks/permission.ts(节选)
a***essRouter(route) {
return !route.meta?.requiresAuth
|| !route.meta?.roles
|| route.meta?.roles?.includes('*')
|| route.meta?.roles?.includes(userStore?.role)
}
- 服务端菜单模式开关与加载
// src/config/settings.json(节选)
{ "menuFromServer": false }
将其改为 true 后,首次访问会基于用户 menuIds 过滤可见菜单:
// src/store/modules/app/index.ts(节选)
async fetchServerMenuConfig() {
const userStore = useUserStore()
this.serverMenu = getRoutes(
[...appRoutes, ...appExternalRoutes],
(userStore.roles && userStore.roles[0].menuIds) || ['0']
)
}
- 过滤算法(
menuIds为后端授权菜单 ID;['0']表示全部放开)
// src/utils/tree.ts(节选)
export const getRoutes = (list, menuIds: string[] = []) => {
if (menuIds && menuIds.length === 1 && menuIds[0] === '0') return list
const fixedRoutes = [], allRoutes = []
for (const router of list) {
const { requiresAuth, ...rest } = router
if (requiresAuth === false) fixedRoutes.push(rest)
if (requiresAuth === true || menuIds.includes(rest.meta?.id)) {
if (rest.children) rest.children = getRoutes(rest.children, menuIds)
allRoutes.push(rest)
}
}
return [...fixedRoutes, ...allRoutes]
}
- 列/按钮权限(面向
operationIds)
// src/hooks/permission.ts(节选)
checkColumnsPermission(value) {
if (!settings.menuFromServer) return true
const { roles } = useUserStore()
return (roles && roles[0].operationIds.includes(value))
|| (roles[0].operationIds?.length === 1 && roles[0].operationIds[0] === '0')
}
七、状态管理(Pinia)
- 注入与持久化
// src/store/index.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
export default pinia
- 核心模块
-
app:主题/布局与服务端菜单(fetchServerMenuConfig) -
user:登录、登出、用户信息、角色集
-
八、网络请求封装(Axios)
- 实例化与默认选项
// src/utils/request/index.ts(节选)
export const request = createAxios()
function createAxios(opt?: Partial<CreateAxiosOptions>) {
return new VAxios(merge(<CreateAxiosOptions>{
authenticationScheme: TokenEnum.PREFIX,
timeout: ResultEnum.TIMEOUT,
headers: { 'Content-Type': ContentTypeEnum.JSON },
transform,
requestOptions: {
apiUrl: host, isJoinPrefix: true, urlPrefix: '/api',
isReturnNativeResponse: false, isTransformResponse: true,
joinParamsToUrl: false, formatDate: true, joinTime: true,
ignoreRepeatRequest: true, withToken: true,
retry: { count: 3, delay: 2000 },
},
}, opt || {}))
}
- Token 注入与错误处理(节选)
// src/utils/request/index.ts(节选)
requestInterceptors: (config) => {
const token = getToken()
if (token && (config as Recordable)?.requestOptions?.withToken !== false) {
;(config as Recordable).headers['token'] = `${token}`
}
return config
},
responseInterceptors: (res) => {
if (res?.status !== ResultEnum.SU***ESS) Message.error('网络错误!')
else if (res?.data.code === ResultEnum.SU***ESS) return res
else { Message.error(res?.data.message || '请求错误!'); setTimeout(() => router ? router.push('/login') : (location.href = '/login'), 1000) }
}
- API 使用示例
// src/api/login/index.ts(节选)
export const login = (data: LoginData) => {
return request.post<Result>({ url: '/user/login', data })
}
九、国际化(i18n)
// src/locale/index.ts(节选)
const defaultLocale = localStorage.getItem('arco-locale') || 'zh-***'
const i18n = createI18n({
globalInjection: true,
locale: defaultLocale,
fallbackLocale: 'zh-***',
messages: { 'en-US': en, 'zh-***': *** },
})
十、布局与导航
- 默认布局:导航栏 + 侧边菜单/抽屉 + TabBar + 内容区 + 页脚(可配置)
<!-- src/layout/default-layout.vue(模板节选) -->
<a-layout class="layout" :class="{ mobile: appStore.hideMenu }">
<div v-if="navbar" class="layout-navbar"><NavBar /></div>
<a-layout-sider v-if="renderMenu" v-show="!hideMenu" class="layout-sider" :width="menuWidth" :hide-trigger="true">
<div class="menu-wrapper"><Menu /></div>
</a-layout-sider>
<a-drawer v-if="hideMenu" :visible="drawerVisible" placement="left" :closable="false"><Menu /></a-drawer>
<a-layout class="layout-content" :style="paddingStyle">
<TabBar v-if="appStore.tabBar" />
<a-layout-content><PageLayout /></a-layout-content>
<Footer v-if="footer" />
</a-layout>
</a-layout>
- 全局配置(主题/布局/菜单来源)
// src/config/settings.json(节选)
{
"theme": "light",
"navbar": true,
"menu": true,
"topMenu": false,
"darkMenu": false,
"hideMenu": false,
"menuCollapse": false,
"footer": false,
"themeColor": "#001529",
"menuWidth": 200,
"globalSettings": false,
"tabBar": true,
"menuFromServer": false
}
十一、全局业务组件注册
// src/***ponents/index.ts(节选)
export default {
install(Vue: App) {
Vue.***ponent('MChart', MChart)
Vue.***ponent('MTable', MTable)
Vue.***ponent('MBtnGroup', MBtnGroup)
Vue.***ponent('MDict', MDict)
},
}
组件文档(含 MTable/MForm/季度选择器/MSearchForm 等):M-Admin 组件文档
十二、实战 A:新增页面与菜单(本地路由模式)
- 在
src/router/routes/modules/新增feature.ts,声明父子路由与meta.id/roles - 在
src/views/feature/下新增index.vue - 重启后会自动合入
appRoutes,根据meta自动展示在菜单中
示例:
// src/router/routes/modules/feature.ts
import { DEFAULT_LAYOUT } from '../base'
export default [{
path: '/feature',
name: 'feature',
***ponent: DEFAULT_LAYOUT,
meta: { id: '9000', locale: 'menu.feature', icon: 'apps', requiresAuth: true, roles: ['*'] },
children: [{
path: 'list',
name: 'FeatureList',
***ponent: () => import('@/views/feature/index.vue'),
meta: { id: '9001', locale: 'menu.feature.list', requiresAuth: true, roles: ['*'] },
}],
}]
十三、实战 B:接入后端登录与服务端菜单
- 登录流程
-
userStore.login成功后setToken(res.token)写入本地 - 守卫检测
isLogin(),未登录跳转到login
-
- 服务端菜单
- 打开
settings.json中menuFromServer: true - 用户登录后从后端拿到
roles[0].menuIds与roles[0].operationIds -
appStore.fetchServerMenuConfig()依据menuIds过滤可见菜单
- 打开
- 请求调用建议
- 将接口定义在
src/api/**下,统一复用request实例 - 如需原始响应或禁用 Token,可传入:
{ isReturnNativeResponse: true, withToken: false }
- 将接口定义在
十四、构建与部署
- 生产构建:
pnpm build:prod - 静态托管:将
dist目录发布至静态服务器 - 路由基础路径:默认
createWebHistory(''),若部署到子路径,需同步调整 Vitebase与路由history基础路径
十五、常见问题速查(FAQ)
- 依赖安装失败:确认 Node 版本(≥18.12),建议使用
pnpm - 页面空白/样式异常:确保页面根节点完整;主题/暗黑模式由
appStore控制 - 请求 401:检查 Token 注入、跨域与登录态;拦截器会在异常时引导回登录
- 菜单不显示(服务端菜单模式):核对
roles[0].menuIds是否包含路由meta.id
十六、参考链接
- 官方文档(指南、组件、更新日志):M-Admin 文档