【Vue 的前端框架 M-Admin 的学习与整理】

【Vue 的前端框架 M-Admin 的学习与整理】

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 状态管理(appusertab-bartable 等)
  • src/utils/request/:Axios 二次封装(实例、拦截器、类型、工具)
  • src/***ponents/:全局业务组件(如 MTableMFormMDictMChart
  • 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, [])
  • 路由元信息(示例):idlocaleiconrequiresAuthrolesorderlevel
// 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:新增页面与菜单(本地路由模式)

  1. src/router/routes/modules/ 新增 feature.ts,声明父子路由与 meta.id/roles
  2. src/views/feature/ 下新增 index.vue
  3. 重启后会自动合入 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.jsonmenuFromServer: true
    • 用户登录后从后端拿到 roles[0].menuIdsroles[0].operationIds
    • appStore.fetchServerMenuConfig() 依据 menuIds 过滤可见菜单
  • 请求调用建议
    • 将接口定义在 src/api/** 下,统一复用 request 实例
    • 如需原始响应或禁用 Token,可传入:{ isReturnNativeResponse: true, withToken: false }

十四、构建与部署

  • 生产构建:pnpm build:prod
  • 静态托管:将 dist 目录发布至静态服务器
  • 路由基础路径:默认 createWebHistory(''),若部署到子路径,需同步调整 Vite base 与路由 history 基础路径

十五、常见问题速查(FAQ)

  • 依赖安装失败:确认 Node 版本(≥18.12),建议使用 pnpm
  • 页面空白/样式异常:确保页面根节点完整;主题/暗黑模式由 appStore 控制
  • 请求 401:检查 Token 注入、跨域与登录态;拦截器会在异常时引导回登录
  • 菜单不显示(服务端菜单模式):核对 roles[0].menuIds 是否包含路由 meta.id

十六、参考链接

  • 官方文档(指南、组件、更新日志):M-Admin 文档
转载请说明出处内容投诉
CSS教程网 » 【Vue 的前端框架 M-Admin 的学习与整理】

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买