项目地址: vue-element-admin
集成方案: vue-element-admin
基础模板: vue-admin-template
桌面终端: electron-vue-admin
Typescript版: vue-typescript-admin-template
Others: awesome-project
项目文档: vue-element-admin-docs
项目文档: vue-element-admin-docs
介绍
功能
- 登录 / 注销
- 权限验证
- 页面权限
- 指令权限
- 权限配置
- 二步登录
- 多环境发布
- dev sit stage prod
- 全局功能
- 国际化多语言
- 多种动态换肤
- 动态侧边栏(支持多级路由嵌套)
- 动态面包屑
- 快捷导航(标签页)
- Svg Sprite 图标
- 本地/后端 mock 数据
- Screenfull全屏
- 自适应收缩侧边栏
- 编辑器
- 富文本
- Markdown
- JSON 等多格式
- Excel
- 导出excel
- 导入excel
- 前端可视化excel
- 导出zip
- 表格
- 动态表格
- 拖拽表格
- 内联编辑
- 错误页面
- 401
- 404
- 組件
- 头像上传
- 返回顶部
- 拖拽Dialog
- 拖拽Select
- 拖拽看板
- 列表拖拽
- SplitPane
- Dropzone
- Sticky
- CountTo
- 综合实例
- 错误日志
- Dashboard
- 引导页
- ECharts 图表
- Clipboard(剪贴复制)
- Markdown2html
目录结构
本项目已经为你生成了一个完整的开发框架,提供了涵盖中后台开发的各类功能和坑位,下面是整个项目的目录结构。
├── build # 构建相关
├── mock # 项目mock 模拟数据
├── plop-templates # 基本模板
├── public # 静态资源
│ │── favicon.ico # favicon图标
│ └── index.html # html模板
├── src # 源代码
│ ├── api # 所有请求
│ ├── assets # 主题 字体等静态资源
│ ├── components # 全局公用组件
│ ├── directive # 全局指令
│ ├── filters # 全局 filter
│ ├── icons # 项目所有 svg icons
│ ├── lang # 国际化 language
│ ├── layout # 全局 layout
│ ├── router # 路由
│ ├── store # 全局 store管理
│ ├── styles # 全局样式
│ ├── utils # 全局公用方法
│ ├── vendor # 公用vendor
│ ├── views # views 所有页面
│ ├── App.vue # 入口页面
│ ├── main.js # 入口文件 加载组件 初始化等
│ └── permission.js # 权限管理
├── tests # 测试
├── .env.xxx # 环境变量配置
├── .eslintrc.js # eslint 配置项
├── .babelrc # babel-loader 配置
├── .travis.yml # 自动化CI配置
├── vue.config.js # vue-cli 配置
├── postcss.config.js # postcss 配置
└── package.json # package.json
安装
# 克隆项目
git clone https://github.com/PanJiaChen/vue-element-admin.git
# 进入项目目录
cd vue-element-admin
# 安装依赖
npm install
# 建议不要用 cnpm 安装 会有各种诡异的bug 可以通过如下操作解决 npm 下载速度慢的问题
npm install --registry=https://registry.npmmirror.com
# 本地开发 启动项目
npm run dev
强烈建议不要用直接使用 cnpm 安装,会有各种诡异的 bug,可以通过重新指定 registry 来解决 npm 安装速度慢的问题。若还是不行,可使用 yarn 替代 npm。
Windows 用户若安装不成功,很大概率是node-sass安装失败,解决方案。
另外因为 node-sass 是依赖 python环境的,如果你之前没有安装和配置过的话,需要自行查看一下相关安装教程。
启动完成后会自动打开浏览器访问 http://localhost:9527
接下来你可以修改代码进行业务开发了,本项目内建了典型业务模板、常用业务组件、模拟数据、HMR 实时预览、状态管理、国际化、全局路由等等各种实用的功能来辅助开发,你可以继续阅读和探索左侧的其它文档。
你可以把 vue-element-admin
当做工具箱或者集成方案仓库,在 vue-admin-template
的基础上进行二次开发,想要什么功能或者组件就去 vue-element-admin
那里复制过来。
Vue 生态圈
首先了解这些 vue 生态圈的东西,会对你上手本项目有很大的帮助。
-
Vue Router 是 vue 官方的路由。它能快速的帮助你构建一个单页面或者多页面的项目。
-
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。它能解决你很多全局状态或者组件之间通信的问题。
-
Vue Loader 是为 vue 文件定制的一个 webpack 的 loader,它允许你以一种名为单文件组件 (SFCs)的格式撰写 Vue 组件。它能在开发过程中使用热重载来保持状态,为每个组件模拟出 scoped CSS 等等功能。不过大部分情况下你不需要对它直接进行配置,脚手架都帮你封装好了。
-
Vue Test Utils 是官方提供的一个单元测试工具。它能让你更方便的写单元测试。
-
Vue Dev-Tools Vue 在浏览器下的调试工具。写 vue 必备的一个浏览器插件,能大大的提高你调试的效率。
-
Vue CLI 是官方提供的一个 vue 项目脚手架,本项目也是基于它进行构建的。它帮你封装了大量的 webpack、babel 等其它配置,让你能花更少的精力在搭建环境上,从而能更专注于页面代码的编写。不过所有的脚手架都是针对大部分情况的,所以一些特殊的需求还是需要自己进行配置。建议先阅读一遍它的文档,对一些配置有一些基本的了解。
-
Vetur 是 VS Code 的插件. 如果你使用 VS Code 来写 vue 的话,这个插件是必不可少的。
布局
页面整体布局是一个产品最外层的框架结构,往往会包含导航、侧边栏、面包屑以及内容等。想要了解一个后台项目,先要了解它的基础布局。
@
是 webpack
的 alias
Layout
vue-element-admin 中大部分页面都是基于这个 layout 的,除了个别页面如:login , 404, 401 等页面没有使用该layout。如果你想在一个项目中有多种不同的layout也是很方便的,只要在一级路由那里选择不同的layout组件就行。
// No layout
{
path: '/401',
component: () => import('errorPage/401')
}
// Has layout
{
path: '/documentation',
// 你可以选择不同的layout组件
component: Layout,
// 这里开始对应的路由都会显示在app-main中 如上图所示
children: [{
path: 'index',
component: () => import('documentation/index'),
name: 'documentation'
}]
}
这里使用了 vue-router 路由嵌套, 所以一般情况下,你增加或者修改页面只会影响 app-main这个主体区域。其它配置在 layout 中的内容如:侧边栏或者导航栏都是不会随着你主体页面变化而变化的。
/foo /bar
+------------------+ +-----------------+
| layout | | layout |
| +--------------+ | | +-------------+ |
| | foo.vue | | +------------> | | bar.vue | |
| | | | | | | |
| +--------------+ | | +-------------+ |
+------------------+ +-----------------+
当然你也可以一个项目里面使用多个不同的 layout,只要在你想作用的路由父级上引用它就可以了。
app-main
这里在 app-main
外部包了一层 keep-alive
主要是为了缓存 <router-view>
的,配合页面的 tabs-view
标签导航使用,如不需要可自行去除。
其中transition
定义了页面之间切换动画,可以根据自己的需求,自行修改转场动画。相关文档。默认提供了fade
和fade-transform
两个转场动画,具体 css
实现见transition.scss
。如果需要调整可在AppMain.vue
中调整transition
的 name
。
router-view
我创建和编辑的页面使用的是同一个 component,默认情况下这两个页面切换时并不会触发 vue 的 created 或者 mounted 钩子,官方说你可以通过 watch $route 的变化来进行处理,但说真的还是蛮麻烦的。后来发现其实可以简单的在 router-view 上加上一个唯一的 key,来保证路由切换时都会重新渲染触发钩子了。这样简单的多了。
<router-view :key="key"></router-view>
computed: {
key() {
// 只要保证 key 唯一性就可以了,保证不同页面的 key 不相同
return this.$route.fullPath
}
}
或者 可以像本项目中 editForm 和 createForm 声明两个不同的 view 但引入同一个 component。
<!-- create.vue -->
<template>
<article-detail :is-edit='false'></article-detail> //create
</template>
<script>
import ArticleDetail from './components/ArticleDetail'
</script>
<!-- edit.vue -->
<template>
<article-detail :is-edit='true'></article-detail> //edit
</template>
<script>
import ArticleDetail from './components/ArticleDetail'
</script>
移动端
element-ui
官方对自己的定位是桌面端后台框架,而且对于管理后台这种重交互的项目来说是不能通过简单的适配来满足桌面端和移动端两端不同的交互,所以真要做移动版后台,建议重新做一套系统。
所以本项目也不会适配移动端,只是用media query
做了一点简单的响应式布局,有需求请自行修改。
国际化
国际化 本项目集合了国际化 i18n 方案。通过 vue-i18n而实现。
由于本项目 ui 框架使用了element,所以国际化的同时也要将其国际化。 完整代码。 同时将当前 lang 语言存在 cookie之中,为了下次打开页面能记住上次的语言设置。
全局 lang
代码地址: @/lang
目前配置了英文和中文两种语言。
同时在 @/lang/index.js
中引入了 element-ui
的语言包
使用:
// $t 是 vue-i18n 提供的全局方法,更多信息请查看其文档
$t('login.title')
异步 lang
有一些某些特定页面才需要的 lang
,比如 @/views/i18n-demo
页面
import local from './local'
this.$i18n.mergeLocaleMessage('en', local.en)
this.$i18n.mergeLocaleMessage('zh', local.zh)
js 中使用 $t
如果你使用如 select
组件,它的值是通过 v-for
而来,如:
<el-select v-model="value">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"/>
</el-select>
this.options = [
{
value: '1',
label: this.$t('i18nView.one')
},
{
value: '2',
label: this.$t('i18nView.two')
},
{
value: '3',
label: this.$t('i18nView.three')
}
]
这种情况下,国际化只会执行一次,因为在 js 中的this.options
只会在初始化的时候执行一次,它的数据并不会随着你本地 lang
的变化而变化,所以需要你在lang变化的时候手动重设this.options
。
export default {
watch: {
lang() {
this.setOptions()
}
},
methods: {
setOptions() {
this.options = [
{
value: '1',
label: this.$t('i18nView.one')
},
{
value: '2',
label: this.$t('i18nView.two')
},
{
value: '3',
label: this.$t('i18nView.three')
}
]
}
}
}
移除国际化
在 src/main.js
中移除 import i18n from './lang'
并且删除 src/lang
文件夹。
并在 src/layout/components/Levelbar
、src/layout/components/SidebarItem
、src/layout/components/TabsView
等文件夹中 移除 this.$t('route.xxxx')
使用国际化的方式。
在v4.1.0+
版本之后,默认 master 将不再提供国际化。因为大部分用户其实是用不到国际化的,但移除国际化工作量又相当的大。
如果你有国际化需求的请使用 i18n 分支,它与 master
同步更新。
新增页面
如果熟悉 vue-router
的配置就很简单了。
首先在 @/router/index.js
中增加你需要添加的路由。
如:新增一个 excel 页面
{
path: '/excel',
component: Layout,
redirect: '/excel/export-excel',
name: 'excel',
meta: {
title: 'excel',
icon: 'excel'
}
}
仅仅这样不会有任何效果的,它只是创建了一个基于layout
的一级路由,你还需要在它下面的 children
中添加子路由。
{
path: '/excel',
component: Layout,
redirect: '/excel/export-excel',
name: 'excel',
meta: {
title: 'excel',
icon: 'excel'
},
children: [
{
path: 'export-excel',
component: ()=>import('excel/exportExcel'),
name: 'exportExcel',
meta: { title: 'exportExcel' }
}
]
}
这样侧边栏就会出现 menu-item
了
由于 children
下面只声明了一个路由所以不会有展开箭头,如果children下面的路由个数大于 1 就会有展开箭头,如下面所示。 如果你想忽视这个自动判断可以使用 alwaysShow: true
,这样它就会忽略之前定义的规则,一直显示根路由。详情见路由和侧边栏
{
path: '/excel',
component: Layout,
redirect: '/excel/export-excel',
name: 'excel',
meta: {
title: 'excel',
icon: 'excel'
},
children: [
{ path: 'export-excel', component: ()=>import('excel/exportExcel'), name: 'exportExcel', meta: { title: 'exportExcel' }},
{ path: 'export-selected-excel', component: ()=>import('excel/selectExcel'), name: 'selectExcel', meta: { title: 'selectExcel' }},
{ path: 'upload-excel', component: ()=>import('excel/uploadExcel'), name: 'uploadExcel', meta: { title: 'uploadExcel' }}
]
}
侧边栏就会出现 submenu
了
多级目录(嵌套路由)
如果你的路由是多级目录,如本项目 @/views/nested
那样, 有三级路由嵌套的情况下,不要忘记还要手动在二级目录的根文件下添加一个 <router-view>
。
<!-- 父级路由组件 -->
<template>
<div>
<!-- xxx html 内容 -->
<router-view />
</div>
</template>
新增 view
新增完路由之后不要忘记在 @/views
文件下 创建对应的文件夹,一般一个路由对应一个文件,该模块下的功能组件或者方法就建议在本文件夹下创建一个utils或components文件夹,各个功能模块维护自己的utils或components组件。
新增 api
最后在 @/api
文件夹下创建本模块对应的 api 服务。
新增组件
个人写 vue 项目的习惯,在全局的 @/components
只会写一些全局的组件,如富文本,各种搜索组件,封装的日期组件等等能被公用的组件。每个页面或者模块特定的业务组件则会写在当前 views
下面。如:@/views/article/components/xxx.vue。这样拆分大大减轻了维护成本。
请记住拆分组件最大的好处不是公用而是可维护性!
新增样式
页面的样式和组件是一个道理,全局的 @/style
放置一下全局公用的样式,每一个页面的样式就写在当前 views
下面,请记住加上scoped
或者命名空间,避免造成全局的样式污染。
<style>
/* global styles */
</style>
<style scoped>
/* local styles */
.xxx-container{
/* name scoped */
xxx
}
</style>
样式
CSS Modules
在样式开发过程中,有两个问题比较突出:
-
全局污染 —— CSS 文件中的选择器是全局生效的,不同文件中的同名选择器,根据 build 后生成文件中的先后顺序,后面的样式会将前面的覆盖;
-
选择器复杂 —— 为了避免上面的问题,我们在编写样式的时候不得不小心翼翼,类名里会带上限制范围的标示,变得越来越长,多人开发时还很容易导致命名风格混乱,一个元素上使用的选择器个数也可能越来越多,最终导致难以维护。
好在 vue 为我们提供了 scoped 可以很方便的解决上述问题。 它顾名思义给 css 加了一个域的概念。
/* 编译前 */
.example {
color: red;
}
/* 编译后 */
.example[_v-f3f3eg9] {
color: red;
}
只要加上 <style scoped>
这样 css
就只会作用在当前组件内了。详细文档见 vue-loader
使用 scoped 后,父组件的样式将不会渗透到子组件中。不过一个子组件的根节点会同时受其父组件的 scoped CSS
和子组件的 scoped CSS
的影响。这样设计是为了让父组件可以从布局的角度出发,调整其子组件根元素的样式。
目录结构
vue-element-admin· 所有全局样式都在
@/styles` 下设置
├── styles
│ ├── btn.scss # 按钮样式
│ ├── element-ui.scss # 全局自定义 element-ui 样式
│ ├── index.scss # 全局通用样式
│ ├── mixin.scss # 全局mixin
│ ├── sidebar.scss # sidebar css
│ ├── transition.scss # vue transition 动画
│ └── variables.scss # 全局变量
常见的工作流程是,全局样式都写在 src/styles
目录下,每个页面自己对应的样式都写在自己的 .vue
文件之中
<style>
/* global styles */
</style>
<style scoped>
/* local styles */
</style>
自定义 element-ui 样式
现在我们来说说怎么覆盖 element-ui
样式。由于 element-ui
的样式我们是在全局引入的,所以你想在某个页面里面覆盖它的样式就不能加 scoped
,但你又想只覆盖这个页面的 element
样式,你就可在它的父级加一个 class
,用命名空间来解决问题。
.article-page {
/* 你的命名空间 */
.el-tag {
/* element-ui 元素*/
margin-right: 0px;
}
}
当然也可以使用深度作用选择器 下文会介绍
父组件改变子组件样式 深度选择器
当你子组件使用了 scoped
但在父组件又想修改子组件的样式可以 通过 >>> 来实现:
<style scoped>
.a >>> .b { /* ... */ }
</style>
将会编译成
.a[data-v-f3f3eg9] .b {
/* ... */
}
如果你使用了一些预处理的东西,如 sass
你可以通过 /deep/
来代替 >>>
实现想要的效果。
所以你想覆盖某个特定页面 element
的 button
的样式,你可以这样做:
.xxx-container >>> .el-button{
xxxx
}
Autoprefixer [新版本已无该问题]
vue-cli 有一个小坑,它默认 autoprefixer 只会对通过 vue-loader 引入的样式才会有有作用,换而言之也就是 .vue 文件里面的 css autoprefixer 才会效果。
//app.vue
<style lang="scss">
@import './styles/index.scss'; // 全局自定义的css样式
</style>
你在 .vue
文件中引入你要的样式就可以了,或者你可以改变 vue-cli
的文件在 css-loader
前面在加一个 postcss-loader
,在前面的 issue
地址中已经给出了解决方案。不过新版本已经默认解决处理了这个问题。
Postcss
这里再来说一下 postcss
的配置问题,新版的 vue-cli webpack 模板 init
之后根目录下默认有一个postcss.config.js
。vue-loader
的 postcss
会默认读取这个文件的里的配置项,所以在这里直接改配置文件就可以了。配置和 postcss
是一样的。
// postcss.config.js
module.exports = {
plugins: {
autoprefixer: {}
}
}
// package.json
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
如上面代码所述的,autoprefixer
会去读取 package.json
下 browserslist
的配置参数
- 1% 兼容全球使用率大于 1%的浏览器
- last 2 versions 兼容每个浏览器的最近两个版本
- not ie <= 8 不兼容 ie8 及以下 具体可见 browserslist
postcss
也还有很多很多其它的功能大家可以自行去探究
Mixin
本项目没有设置自动注入 sass
的 mixin
到全局,所以需要在使用的地方手动引入 mixin
<style rel="stylesheet/scss" lang="scss">
@import "src/styles/mixin.scss";
</style>
如需要自动将 mixin
注入到全局 ,可以使用sass-resources-loader
和服务端进行交互
前端请求流程
在 vue-element-admin
中,一个完整的前端 UI
交互到服务端处理流程是这样的:
- UI 组件交互操作;
- 调用统一管理的 api service 请求函数;
- 使用封装的 request.js 发送请求;
- 获取服务端返回;
- 更新 data;
从上面的流程可以看出,为了方便管理维护,统一的请求处理都放在 @/api
文件夹中,并且一般按照 model
纬度进行拆分文件,如:
api/
login.js
article.js
remoteSearch.js
...
request.js
其中,@/utils/request.js
是基于 axios
的封装,便于统一处理 POST
,GET
等请求参数,请求头,以及错误提示信息等。具体可以参看 request.js
。 它封装了全局 request拦截器
、response拦截器
、统一的错误处理
、统一做了超时处理
、baseURL设置
等。
一个请求文章列表页的例子:
// api/article.js
import request from '../utils/request';
export function fetchList(query) {
return request({
url: '/article/list',
method: 'get',
params: query
})
}
// views/example/list
import { fetchList } from '@/api/article'
export default {
data() {
list: null,
listLoading: true
},
methods: {
fetchData() {
this.listLoading = true
fetchList().then(response => {
this.list = response.data.items
this.listLoading = false
})
}
}
}
设置多个 baseURL
我们可以通过环境变量设置多个baseURL
,从而请求不同的 api
地址。
# .env.development
VUE_APP_BASE_API = '/dev-api' #注入本地 api 的根路径
VUE_APP_BASE_API2 = '/dev-api2' #注入本地 api2 的根路径
之后根据环境变量创建axios
实例,让它具有不同的baseURL
。 @/utils/request.js
// create an axios instance
const service = axios.create({
baseURL: process.env.BASE_API, // api 的 base_url
timeout: 5000 // request timeout
})
const service2 = axios.create({
baseURL: process.env.BASE_API2, // api2 的 base_url
timeout: 5000 // request timeout
})
或者
export function fetchList(query) {
return request({
url: '/article/list',
method: 'get',
params: query,
baseURL: 'xxxx' // 直接通过覆盖的方式
})
}
从 mock 直接切换到服务端请求
见 Mock 和联调