简介:这份资源是面向前端开发者与企业后台项目搭建者的 Vue2 后台管理模板,基于 vue-element-admin 框架,整合 Vue.js 2.x 的响应式数据绑定与组件化能力,以及 Element UI 的表格、表单、提示等常用组件,帮助开发者省去从零搭建的重复工作,快速构建可复用的企业级管理系统。压缩包共 324 个文件,以 png、svg 图片素材和 md 说明文档为主,另含 js 脚本、zip 子包、docx 项目笔记及少量 vue、scss 源码文件,整体约 526.69MB,素材与文档齐备,便于对照查阅。资源涵盖路由懒加载与动态权限、Vuex 全局状态管理、基于角色的权限控制、axios 接口封装、Mock 数据模拟、国际化、图标库、日志记录以及 Webpack 构建与测试方案等模块,并附有权限系统、单点登录、人资项目总结等文档,可帮助读者理解框架分层结构、按需配置扩展并优化性能。目前已有 261 人学习下载,适合具备一定 Vue 基础、希望提升后台开发效率的开发者参考。
1. 后台管理系统的“最后一公里”:为什么我还在翻这份 Vue2 老框架
去年接手一个运营后台的二次开发,需求不复杂:加三个页面、接一套权限、把表格导出做掉。我第一反应是上 Vue3 + Vite,结果甲方一句“服务器上跑的还是老环境,Node 版本别动”把我按回现实。翻了一圈,最后还是回到 vue-element-admin 这套基于 Vue2 的后台模板上——它把登录鉴权、动态路由、按钮级权限、表格分页、表单校验这些后台系统里最磨人的“最后一公里”全铺好了,你拿到手不是一张白纸,而是一个能直接登录进去点菜单的壳子。适合谁?适合要在一两周内交付一个中后台、又不想从零搭权限体系的前端;也适合想读懂“一个成熟后台框架到底怎么组织代码”的开发者。下面我按自己拆项目的顺序,从目录结构一路讲到怎么改,能跑通也能上手改。
2. 目录结构与技术栈:先看清这套壳子的骨架
拿到一个陌生框架,我习惯先不急着npm install,而是把目录树拉出来看一遍。vue-element-admin 的目录组织非常“教科书”,但它的教科书感恰恰是优点——每个目录职责单一,你改哪块基本不会牵一发动全身。这一章先把技术栈和目录讲透,后面读代码才有坐标。
2.1 技术栈清单与版本约束
这套框架的核心依赖是 Vue 2.6、Vue Router 3、Vuex 3、Element UI 2,构建工具是 Vue CLI(webpack 体系),请求层用 axios,权限校验用 js-cookie 存 token,图标用 svg-sprite-loader 做雪碧图。它没有用 TypeScript,全部是.vue+.js,这对不熟 TS 的团队反而是降低门槛。
| 依赖 | 作用 | 二次开发时的注意点 |
|---|---|---|
| vue 2.6 | 视图层 | 别升 2.7,组合式 API 会带来写法混乱 |
| vue-router 3 | 路由 | 动态路由靠addRoutes,不是addRoute |
| vuex 3 | 全局状态 | 用户信息、权限、菜单都在这 |
| element-ui 2 | UI 组件库 | 主题色改 SCSS 变量,别直接覆盖类名 |
| axios | 请求封装 | 拦截器在src/utils/request.js |
| js-cookie | token 存储 | 默认存 Cookie,可换 localStorage |
提示:这套框架锁死 Vue2 生态,Node 建议 14/16,Node 18 以上装依赖时 node-sass 容易编译失败,这是最常见的第一个坑。
2.2 目录职责拆解
src/ ├── api/ # 接口定义,按业务模块拆文件 ├── assets/ # 静态资源,图片、字体 ├── components/ # 全局复用组件(面包屑、上传、富文本等) ├── layout/ # 整体布局:侧边栏、顶栏、标签页 ├── router/ # 路由表 + 动态路由逻辑 │ ├── index.js # 常量路由(登录、404) │ └── modules/ # 业务路由,按模块拆 ├── store/ # Vuex:user、permission、app、settings ├── styles/ # 全局 SCSS 变量与混入 ├── utils/ # request、auth、validate 等工具 └── views/ # 页面组件,和路由一一对应这个结构里最关键的是router和store的配合:router/index.js只放不需要权限的常量路由,业务路由放在router/modules下,登录后由store/modules/permission.js根据后端返回的权限动态addRoutes。layout目录负责“壳”,views负责“内容”,两者通过<router-view>嵌套。理解了这个分层,你加页面时就知道该往哪放:页面进views,路由进modules,接口进api,三者一一对应,别混着写。
2.3 启动脚本与本地跑通
依赖装完,package.json里的脚本不多但够用:
# 安装依赖,建议锁 Node 14/16 npm install # 本地开发,默认 9528 端口 npm run dev # 打包,产物在 dist/ npm run build:prod # 代码检查,提交前跑一遍 npm run lintnpm run dev起来后默认会打开登录页,账号密码在mock/user.js里写死(admin / 111111),因为框架自带 mock 服务,不接后端也能把整套流程点一遍。这一步很重要——先让 mock 跑通,确认菜单、权限、表格都正常,再去接真实接口,出问题时你才知道是框架问题还是后端问题。我第一次接后端时跳过了 mock 直接改request.js的 baseURL,结果登录一直 401,排查半天发现是 mock 没关、请求被拦截了,血泪经验。
3. 登录鉴权与动态路由:一个请求从进入到返回的完整链路
后台系统最核心也最容易翻车的就是权限。vue-element-admin 的权限分两层:路由级(能不能进这个页面)和按钮级(页面里某个按钮显不显示)。这一章我把登录到菜单渲染的完整链路拆开讲,每段代码都标清楚改哪里。
3.1 登录流程与 token 落库
登录页views/login/index.vue提交表单后调store/modules/user.js的loginaction:
// store/modules/user.js const actions = { login({ commit }, userInfo) { const { username, password } = userInfo return new Promise((resolve, reject) => { login({ username: username.trim(), password: password }) .then(response => { const { data } = response // token 同时写 Vuex 和 Cookie,刷新页面靠 Cookie 恢复 commit('SET_TOKEN', data.token) setToken(data.token) resolve() }) .catch(error => reject(error)) }) } }login来自api/user.js,内部走utils/request.js的 axios 实例。这里的关键是 token 双写:Vuex 里存一份供组件读,Cookie 里存一份供刷新恢复。utils/auth.js封装了getToken/setToken/removeToken,默认用 js-cookie,过期时间在setToken里配。如果你要改成 localStorage,只改这一个文件,别到处散着写。
请求拦截器负责给每个请求挂 token:
// utils/request.js service.interceptors.request.use( config => { if (store.getters.token) { // 让每个请求携带 token,后端按 header 校验 config.headers['X-Token'] = getToken() } return config }, error => Promise.reject(error) )响应拦截器统一处理业务码,code !== 20000就弹错误并 reject,401 时清 token 跳登录。这套约定你要接的后端得对齐——要么后端按这个码返回,要么你改拦截器里的判断,别两边各写一套。
3.2 动态路由生成:addRoutes 的时机
登录成功后,路由守卫permission.js拦截跳转,发现没拉过用户信息,就调store.dispatch('user/getInfo')拿角色,再调store.dispatch('permission/generateRoutes', roles)生成可访问路由:
// store/modules/permission.js const actions = { generateRoutes({ commit }, roles) { return new Promise(resolve => { let accessedRoutes if (roles.includes('admin')) { // 管理员直接拿全量异步路由 accessedRoutes = asyncRoutes || [] } else { // 普通角色按 meta.roles 过滤 accessedRoutes = filterAsyncRoutes(asyncRoutes, roles) } commit('SET_ROUTES', accessedRoutes) resolve(accessedRoutes) }) } }filterAsyncRoutes递归遍历asyncRoutes,检查每个路由的meta.roles是否和当前角色有交集,有就保留,没有就剔除,子路由同理。生成完在守卫里router.addRoutes(accessRoutes)动态挂载。注意是addRoutes(复数),Vue Router 3 的 API,别写成 Vue Router 4 的addRoute,这是升级时最常见的报错来源。
3.3 按钮级权限:v-permission 指令
页面级权限靠路由,按钮级靠自定义指令directives/permission.js:
// 用法:<el-button v-permission="['user:add']">新增</el-button> export default { inserted(el, binding) { const { value } = binding const roles = store.getters && store.getters.roles if (value && value instanceof Array && value.length > 0) { const permissionRoles = value // 当前用户权限里是否包含指令要求的任一权限 const hasPermission = roles.some(role => permissionRoles.includes(role)) if (!hasPermission) { // 没权限直接从 DOM 移除,不是隐藏 el.parentNode && el.parentNode.removeChild(el) } } } }这里有个细节:它是removeChild而不是display:none,所以没权限的按钮在 DOM 里根本不存在,抓包也看不到,安全性比隐藏好。但反过来,如果你用v-if控制按钮又叠加这个指令,可能因为渲染顺序导致指令拿不到父节点报错,我一般统一用指令,不再混v-if。
4. 布局与状态管理:侧边栏、标签页和 Vuex 的分工
后台系统的“壳”决定了使用体验,vue-element-admin 的 layout 做得比较完整:可折叠侧边栏、顶部面包屑、多标签页、全屏、换肤。这一章讲布局组件怎么组织,以及 Vuex 里几个 module 各管什么,改的时候不至于把状态搅成一锅粥。
4.1 Layout 组件树与侧边栏渲染
layout/index.vue是外层容器,内部结构大致是:Sidebar(侧边栏)+Navbar(顶栏)+AppMain(内容区)+TagsView(标签页)。侧边栏layout/components/Sidebar/index.vue从store.getters.permission_routes拿路由表,递归渲染SidebarItem,遇到有children的渲染el-submenu,没有的渲染el-menu-item。菜单的显示隐藏靠路由meta.hidden控制,meta.title是菜单名,meta.icon是图标。
// store/modules/permission.js 的 getters const getters = { permission_routes: state => state.permission.routes, sidebar: state => state.app.sidebar }改菜单顺序不用动组件,调router/modules里数组的顺序就行;改菜单名改meta.title;想隐藏某个页面不进菜单,加meta: { hidden: true }。这套约定很省心,但要注意meta.title如果配了 i18n 的 key,得去lang目录补翻译,否则菜单显示的是 key 原文,我第一次就踩了这个。
4.2 Vuex 四个 module 的职责边界
store/modules下有app、user、permission、settings四个模块,分工很清晰:
app:侧边栏折叠状态、设备类型,纯 UI 状态;user:token、用户信息、角色,登录相关;permission:动态路由表,权限相关;settings:主题色、是否固定 header、是否显示 tagsView,配置相关。
改的时候守住边界:UI 状态进app,别把业务数据塞进去;用户相关进user;权限路由只进permission。我见过有人把表格数据也放 Vuex,结果页面一多状态互相污染,最后只能推倒重来。后台系统的列表数据放组件本地data就够了,Vuex 只放跨页面共享的东西。
4.3 标签页与 keep-alive 缓存
TagsView记录访问过的页面,配合AppMain里的<keep-alive>做缓存:
// layout/components/AppMain.vue <keep-alive :include="cachedViews"> <router-view :key="key" /> </keep-alive>cachedViews来自store/modules/tagsView.js,页面name必须和路由name一致才会被缓存,这是硬约束。很多人加了标签页发现切回来数据还是重新请求,八成是组件name和路由name对不上。另外:key="key"默认用route.path,同一路由不同参数会复用组件,如果你需要按参数刷新,把 key 改成route.fullPath。
5. 接口层与表格表单:业务页面怎么照着抄
框架给的是壳,真正干活的是业务页面。这一章拿最常见的“列表 + 分页 + 搜索 + 导出”场景,讲接口层怎么封装、表格怎么组织,照着改就能出活。
5.1 request.js 封装与接口文件规范
所有接口走utils/request.js的 axios 实例,api/下按模块建文件,一个函数一个接口:
// api/user.js import request from '@/utils/request' // 分页查用户列表 export function getUserList(params) { return request({ url: '/user/list', method: 'get', params // GET 用 params,POST 用 data,别搞反 }) } // 新增用户 export function addUser(data) { return request({ url: '/user/add', method: 'post', data }) }约定是:GET 传params,POST/PUT 传data,返回值统一是response.data(拦截器里已经剥了一层)。这样页面里getUserList({ page: 1, size: 10 }).then(res => ...)拿到的就是后端业务数据,不用再.data.data套娃。接新后端时如果字段结构不一样,改拦截器里的返回处理,别在每个页面里改。
5.2 列表页的标准结构
一个标准列表页大致长这样,搜索表单 + 表格 + 分页三件套:
// views/user/index.vue 的 script 部分 export default { data() { return { list: [], total: 0, listQuery: { page: 1, limit: 10, keyword: '' }, loading: false } }, created() { this.getList() }, methods: { getList() { this.loading = true getUserList(this.listQuery).then(res => { this.list = res.data.list this.total = res.data.total }).finally(() => { this.loading = false // 无论成功失败都关 loading }) }, handleFilter() { this.listQuery.page = 1 // 搜索时重置到第一页 this.getList() } } }几个容易忽略的点:搜索时要把page重置为 1,否则你在第 5 页搜索可能查出空列表;loading用finally关,别只在then里关,接口报错时 loading 会一直转;分页组件的current-page和listQuery.page双向绑定,别各写各的。
5.3 导出与文件下载
导出一般走后端返回文件流,前端用responseType: 'blob'接:
// api/export.js export function exportUser(params) { return request({ url: '/user/export', method: 'get', params, responseType: 'blob' // 关键:告诉 axios 按二进制接 }) } // 页面里触发下载 exportUser(this.listQuery).then(res => { const blob = new Blob([res], { type: 'application/vnd.ms-excel' }) const link = document.createElement('a') link.href = URL.createObjectURL(blob) link.download = '用户列表.xlsx' link.click() URL.revokeObjectURL(link.href) // 用完释放,避免内存泄漏 })注意responseType必须设在请求配置里,设在拦截器里可能被覆盖;另外 blob 下载时如果后端返回的是 JSON 错误信息,前端会下成一个坏文件,稳妥做法是先判断res.type是不是application/json,是就转文本弹错误。
6. 避坑与排查:这套老框架最容易翻车的五个地方
框架越成熟,坑越集中在“版本”和“约定”上。下面五条是我和同事实际踩过的,按现象、原因、解决写清楚,你遇到时能少走弯路。
6.1 依赖装不上,node-sass 编译报错
现象:npm install卡在 node-sass,报gyp ERR!或 Python 相关错误。原因:node-sass 和 Node 版本强绑定,Node 16 以上基本编不过。解决:把 Node 切到 14 或 16,或者把node-sass换成sass(dart-sass),同时把vue.config.js里的sass-loader配置对齐。换 dart-sass 后/deep/要改成::v-deep,否则样式穿透失效。
6.2 刷新页面 404 或白屏
现象:本地开发正常,部署到服务器后刷新子路由 404。原因:history 模式需要服务端把所有路径回退到index.html,Nginx 没配。解决:Nginx 加try_files $uri $uri/ /index.html;。如果不想动服务端,把router/index.js的mode改成hash,代价是 URL 带#。
6.3 动态路由加了但菜单不显示
现象:addRoutes执行了,侧边栏却没新菜单。原因:侧边栏读的是store.getters.permission_routes,如果你只调了router.addRoutes没 commit 到 store,菜单不会更新。解决:统一走permission/generateRoutes这个 action,它内部既 commit 又返回路由,守卫里拿返回值去addRoutes,两边数据源保持一致。
6.4 keep-alive 不生效,切标签页数据重刷
现象:标签页切回来页面重新请求。原因:组件name和路由name不一致,include匹配不上。解决:检查views下组件export default { name: 'UserList' }和路由name: 'UserList'是否完全一致,大小写都算。另外cachedViews是数组,别写成字符串。
6.5 打包后体积过大、首屏慢
现象:dist几 MB,首屏白屏久。原因:Element UI 全量引入、路由没懒加载、没开 gzip。解决:路由改成component: () => import('@/views/xxx')懒加载;Element UI 按需引入(配babel-plugin-component);Nginx 开gzip on。这三步做完体积通常能砍一半以上。
7. 二次开发改造:从“能用”到“好维护”的几个动作
这套框架拿来直接用没问题,但真要长期维护,我一般会先做几件“打地基”的事,避免后面越改越乱。
第一件是收敛权限判断。框架默认用roles数组做角色判断,但真实项目里权限点往往更细。我的做法是把store/getters里的roles扩展成permissions,登录后由后端返回权限码列表,v-permission指令和路由过滤都改成比对权限码而不是角色名。这样加一个按钮权限不用改代码,后端配一下就行。改造时注意filterAsyncRoutes里的判断逻辑要同步改,否则路由和按钮两套标准会打架。
第二件是给request.js加统一错误上报。拦截器里reject之前,把error.response.status和config.url丢到一个上报函数里,线上出问题能定位是哪个接口挂了。我一般还会加一个请求重试,针对 502/504 这类网关错误重试一次,但只对 GET 重试,POST 重试可能造成重复提交,这个边界要守住。
第三件是抽一个useTable之类的混入(Vue2 用 mixin)。列表页的listQuery、getList、handleFilter、分页逻辑几乎每个页面都一样,抽成mixins/table.js后,页面里只写api方法和列定义,代码量能少三分之一。抽的时候把getList做成可配置的,允许页面覆盖,别抽得太死。
第四件是主题定制。styles/variables.scss里改$--color-primary就能换主色,但 Element UI 的组件样式是编译时注入的,改完要重新npm run build才生效,热更新不一定看得到。如果要做多主题,得用element-theme工具重新生成主题包,这个成本要提前评估。
验证改造是否成功,我的习惯是跑一遍“登录 → 进列表 → 搜索 → 分页 → 导出 → 退出”这条主链路,再故意用一个没权限的账号登一次,确认菜单和按钮都按预期隐藏。这套流程走下来,基本能覆盖权限和请求两条主线。
说个教训:我早期改这套框架时图省事,直接在views里写死接口地址,没走api层,结果后端换域名时改了二十多个文件,漏了一个导致线上白屏。从那以后我每次加页面都强制走api/+request.js这条链路,哪怕只调一个接口也建文件。希望这些能帮到你,少踩几个我已经踩过的坑。
本文还有配套的精品资源,点击获取