一、项目架构设计与技术选型
本教程将基于Vue2生态系统构建一个完整的企业级后台管理系统,涵盖权限控制、数据可视化、复杂表单等核心功能。
技术栈组成:
- 核心框架:Vue 2.6 + Composition API
- 状态管理:Vuex 3.x
- 路由系统:Vue-router 3.x
- UI组件库:Element UI 2.x
- HTTP客户端:Axios
- 可视化库:ECharts 5.x
系统功能模块:
- RBAC权限控制系统
- 动态路由与菜单生成
- 多标签页导航
- 数据可视化仪表盘
- 复杂表单解决方案
- Excel导入导出
- 系统主题定制
二、项目初始化与工程配置
1. 创建Vue2项目
# 使用Vue CLI创建项目
vue create admin-system
# 添加必要依赖
cd admin-system
vue add router
vue add vuex
npm install element-ui axios echarts vue-echarts xlsx file-saver
2. 项目目录结构
src/
├── api/ # API接口管理
├── assets/ # 静态资源
├── components/ # 公共组件
│ ├── Layout/ # 布局组件
│ ├── Charts/ # 图表组件
│ └── ...
├── directives/ # 自定义指令
├── filters/ # 全局过滤器
├── router/ # 路由配置
│ ├── index.js # 路由入口
│ ├── modules/ # 路由模块
│ └── permission.js # 权限控制
├── store/ # Vuex状态管理
│ ├── modules/ # 模块化store
│ └── index.js # store入口
├── styles/ # 全局样式
├── utils/ # 工具函数
├── views/ # 页面组件
│ ├── dashboard/ # 仪表盘
│ ├── system/ # 系统管理
│ └── ...
├── App.vue # 根组件
└── main.js # 应用入口
3. 全局配置 (main.js)
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
// 引入全局样式
import '@/styles/index.scss'
// 注册全局组件
import * as directives from '@/directives'
import * as filters from '@/filters'
Object.keys(directives).forEach(key => {
Vue.directive(key, directives[key])
})
Object.keys(filters).forEach(key => {
Vue.filter(key, filters[key])
})
Vue.use(ElementUI, {
size: 'medium'
})
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
三、核心功能实现
1. 权限控制系统
创建路由权限控制 (router/permission.js):
import router from './index'
import store from '@/store'
import { Message } from 'element-ui'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
NProgress.configure({ showSpinner: false })
const whiteList = ['/login', '/auth-redirect']
router.beforeEach(async (to, from, next) => {
NProgress.start()
// 确定用户是否已登录
const hasToken = store.getters.token
if (hasToken) {
if (to.path === '/login') {
next({ path: '/' })
NProgress.done()
} else {
// 检查用户是否已获取权限信息
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
next()
} else {
try {
// 获取用户信息
const { roles } = await store.dispatch('user/getInfo')
// 根据角色生成动态路由
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
// 添加路由
router.addRoutes(accessRoutes)
// 确保addRoutes完成
next({ ...to, replace: true })
} catch (error) {
// 获取信息失败则退出登录
await store.dispatch('user/resetToken')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
} else {
if (whiteList.includes(to.path)) {
next()
} else {
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
router.afterEach(() => {
NProgress.done()
})
2. 动态路由配置
创建路由模块 (router/modules/admin.js):
const Layout = () => import('@/layout')
export default {
path: '/admin',
component: Layout,
redirect: '/admin/user',
meta: { title: '系统管理', icon: 'el-icon-s-tools' },
children: [
{
path: 'user',
component: () => import('@/views/admin/user'),
name: 'UserManagement',
meta: { title: '用户管理', roles: ['admin'] }
},
{
path: 'role',
component: () => import('@/views/admin/role'),
name: 'RoleManagement',
meta: { title: '角色管理', roles: ['admin'] }
}
]
}
四、Vuex状态管理
1. 模块化状态设计
创建用户模块 (store/modules/user.js):
import { login, logout, getInfo } from '@/api/user'
import { getToken, setToken, removeToken } from '@/utils/auth'
const state = {
token: getToken(),
name: '',
avatar: '',
roles: []
}
const mutations = {
SET_TOKEN: (state, token) => {
state.token = token
},
SET_NAME: (state, name) => {
state.name = name
},
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
},
SET_ROLES: (state, roles) => {
state.roles = roles
}
}
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
commit('SET_TOKEN', data.token)
setToken(data.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
// 获取用户信息
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.token).then(response => {
const { data } = response
if (!data) {
reject('验证失败,请重新登录')
}
const { roles, name, avatar } = data
if (!roles || roles.length {
reject(error)
})
})
},
// 用户退出
logout({ commit, state }) {
return new Promise((resolve, reject) => {
logout(state.token).then(() => {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
removeToken()
resolve()
}).catch(error => {
reject(error)
})
})
},
// 移除token
resetToken({ commit }) {
return new Promise(resolve => {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
removeToken()
resolve()
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
五、复杂组件开发
1. 多标签页导航组件
<template>
<div class="tags-view-container">
<scroll-pane class="tags-view-wrapper" ref="scrollPane">
<router-link
v-for="tag in visitedViews"
ref="tag"
:key="tag.path"
:class="isActive(tag)?'active':''"
:to="{ path: tag.path, query: tag.query }"
class="tags-view-item"
@click.middle.native="closeSelectedTag(tag)"
@contextmenu.prevent.native="openMenu(tag,$event)">
{{ tag.title }}
<span
v-if="!isAffix(tag)"
class="el-icon-close"
@click.prevent.stop="closeSelectedTag(tag)" />
</router-link>
</scroll-pane>
<ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu">
<li @click="refreshSelectedTag(selectedTag)">刷新</li>
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">关闭</li>
<li @click="closeOthersTags">关闭其他</li>
<li @click="closeAllTags(selectedTag)">关闭所有</li>
</ul>
</div>
</template>
<script>
import ScrollPane from './ScrollPane'
export default {
components: { ScrollPane },
data() {
return {
visible: false,
top: 0,
left: 0,
selectedTag: {},
affixTags: []
}
},
computed: {
visitedViews() {
return this.$store.state.tagsView.visitedViews
}
},
watch: {
$route() {
this.addTags()
this.moveToCurrentTag()
},
visible(value) {
if (value) {
document.body.addEventListener('click', this.closeMenu)
} else {
document.body.removeEventListener('click', this.closeMenu)
}
}
},
mounted() {
this.initTags()
this.addTags()
},
methods: {
isActive(route) {
return route.path === this.$route.path
},
isAffix(tag) {
return tag.meta && tag.meta.affix
},
addTags() {
const { name } = this.$route
if (name) {
this.$store.dispatch('tagsView/addView', this.$route)
}
},
moveToCurrentTag() {
const tags = this.$refs.tag
if (!tags) return
for (const tag of tags) {
if (tag.to.path === this.$route.path) {
this.$refs.scrollPane.moveToTarget(tag)
break
}
}
},
refreshSelectedTag(view) {
this.$store.dispatch('tagsView/delCachedView', view).then(() => {
const { fullPath } = view
this.$nextTick(() => {
this.$router.replace({
path: '/redirect' + fullPath
})
})
})
},
closeSelectedTag(view) {
this.$store.dispatch('tagsView/delView', view).then(({ visitedViews }) => {
if (this.isActive(view)) {
this.toLastView(visitedViews)
}
})
},
closeOthersTags() {
this.$store.dispatch('tagsView/delOthersViews', this.selectedTag).then(() => {
this.moveToCurrentTag()
})
},
closeAllTags(view) {
this.$store.dispatch('tagsView/delAllViews').then(({ visitedViews }) => {
if (this.affixTags.some(tag => tag.path === view.path)) {
return
}
this.toLastView(visitedViews)
})
},
toLastView(visitedViews) {
const latestView = visitedViews.slice(-1)[0]
if (latestView) {
this.$router.push(latestView)
} else {
this.$router.push('/')
}
},
openMenu(tag, e) {
this.selectedTag = tag
this.visible = true
this.left = e.clientX
this.top = e.clientY
},
closeMenu() {
this.visible = false
},
initTags() {
const affixTags = this.affixTags = this.filterAffixTags(this.$router.options.routes)
for (const tag of affixTags) {
if (tag.name) {
this.$store.dispatch('tagsView/addVisitedView', tag)
}
}
},
filterAffixTags(routes, basePath = '/') {
let tags = []
routes.forEach(route => {
if (route.meta && route.meta.affix) {
const tagPath = path.resolve(basePath, route.path)
tags.push({
fullPath: tagPath,
path: tagPath,
name: route.name,
meta: { ...route.meta }
})
}
if (route.children) {
const childTags = this.filterAffixTags(route.children, route.path)
if (childTags.length >= 1) {
tags = tags.concat(childTags)
}
}
})
return tags
}
}
}
</script>
六、数据可视化实现
1. ECharts集成与封装
<template>
<div class="chart-container" ref="chart" :style="{ height: height }"></div>
</template>
<script>
import echarts from 'echarts'
import resize from './mixins/resize'
export default {
name: 'Chart',
mixins: [resize],
props: {
height: {
type: String,
default: '100%'
},
width: {
type: String,
default: '100%'
},
option: {
type: Object,
required: true
},
theme: {
type: String,
default: 'default'
}
},
data() {
return {
chart: null
}
},
watch: {
option: {
deep: true,
handler(val) {
this.setOptions(val)
}
}
},
mounted() {
this.initChart()
},
beforeDestroy() {
if (!this.chart) return
this.chart.dispose()
this.chart = null
},
methods: {
initChart() {
this.chart = echarts.init(this.$refs.chart, this.theme)
this.setOptions(this.option)
},
setOptions(option) {
if (this.chart) {
this.chart.setOption(option, true)
}
}
}
}
</script>
七、项目优化与部署
1. 生产环境优化
- 代码分割:配置路由懒加载
- CDN加速:外部引入公共库
- Gzip压缩:配置nginx启用gzip
- 缓存策略:合理配置静态资源缓存
2. Docker部署配置
# Dockerfile
FROM nginx:alpine
# 复制构建好的文件
COPY dist /usr/share/nginx/html
# 复制nginx配置
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 暴露端口
EXPOSE 80
# 启动nginx
CMD ["nginx", "-g", "daemon off;"]
# nginx.conf
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend:3000;
proxy_set_header Host $host;
}
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
}
八、总结与扩展
本教程详细介绍了基于Vue2的企业级后台管理系统开发:
- 搭建了完整的项目架构
- 实现了RBAC权限控制系统
- 开发了动态路由与多标签页
- 集成了数据可视化功能
- 优化了生产环境部署方案
扩展学习方向:
- 微前端架构改造
- SSR服务端渲染
- 自动化测试方案
- CI/CD持续集成
完整项目代码已上传GitHub:https://github.com/example/vue2-admin-system