vue-element-admin
Posted 暑假过期le
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了vue-element-admin相关的知识,希望对你有一定的参考价值。
vue-element-admin笔记
目录结构
├── 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.npm.taobao.org
# 本地开发 启动项目
npm run dev
登录
登录表单校验
验证输入内容是否符合规定定义规则,如果不满足通过callback方法返回错误信息
点击登录按钮进行最后的验证 和存储token
路由侧边栏
新建模块的页面
├── dashboard # 首页
├── login # 登录
├── 404 # 404
├── Users # 用户
├── Roles # 角色
├── Rights # 权限
├── Goods # 商品
├── Category # 类别
├── Report # 报表
新建路由
在 router 目录下新建目录 modules
在此目录中新建各个路由模块
路由模块目录结构
路由规则
{
path: '/permission',
component: Layout,
redirect: '/permission/index', //重定向地址,在面包屑中点击会重定向去的地址
hidden: true, // 不在侧边栏显示
alwaysShow: true, //一直显示根路由
meta: { roles: ['admin','editor'] }, //你可以在根路由设置权限,这样它下面所有的子路由都继承了这个权限
children: [{
path: 'index',
component: ()=>import('permission/index'),
name: 'permission',
meta: {
title: 'permission',
icon: 'lock', //图标
roles: ['admin','editor'], //或者你可以给每一个子路由设置自己的权限
noCache: true // 不会被 <keep-alive> 缓存
}
}]
}
上述代码中,我们用到了meta属性,该属性为一个对象,里面可放置自定义属性,主要用于读取一些配置和参数,并且值得**注意
的是:我们的meta写了二级默认路由上面,而不是一级路由,因为当存在二级路由的时候,访问当前路由信息访问的就是二级默认路由
路由分为两种,constantRoutes 和 asyncRoutes。
constantRoutes: 代表那些不需要动态判断权限的路由,如登录页、404、等通用页面。
asyncRoutes: 代表那些需求动态判断权限并通过 addRoutes 动态添加的页面。
静态路由和动态路由临时合并,形成左侧菜单
目标:将静态路由和动态路由的路由表进行连视合并
什么是临时合并
动态路由是需要权限进行访问的,但是权限的动态路由访问是很复杂的,我们可以先将 静态路由和动态路由进行合并,不考虑权限问题,
在 Sidebar 中已经帮你做了判断,当你一个路由下面的 children 声明的路由大于>1 个时,自动会变成嵌套的模式。如果子路
由正好等于一个就会默认将子路由作为根路由显示在侧边栏中
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index'),
meta: { title: '欢迎页面', icon: 'dashboard' }
}]
},
路由主文件
// 引入多个模块的规则
import Layout from '@/layout'
import userRouter from './modules/user'
import roleRouter from './modules/role'
import rightsRouter from './modules/right'
import goodsRouter from './modules/goods'
import categoryRouter from './modules/category'
import reportsRouter from './modules/report'
// 动态路由
export const asyncRoutes = [
userRouter, roleRouter, rightsRouter, goodsRouter, categoryRouter, reportsRouter
]
const createRouter = () => new Router({
// mode: 'history', // require service support
scrollBehavior: () => ({ y: 0 }), // 管理滚动行为 如果出现滚动 切换就让 让页面回到顶部
routes: [...constantRoutes, ...asyncRoutes] // 临时合并所有的路由
})
通过上面的操作,将静态路由和动态路由进行了合并
用户导出功能
Excel 的导入导出都是依赖于js-xlsx来实现的。
在 js-xlsx
的基础上又封装了Export2Excel.js来方便导出数据。
安装excel所需依赖和按需加载
需要安装如下命令:
npm install xlsx file-saver -S
npm install script-loader -S -D
xlsx 在导入功能时已经安装,这里可以不安装
由于js-xlsx
体积还是很大的,导出功能也不是一个非常常用的功能,所以使用的时候建议使用懒加载。使用方法如下:
import('@/vendor/Export2Excel').then(excel => {
excel.export_json_to_excel({
header: tHeader, //表头 必填
data, //具体数据 必填
filename: 'excel-list', //非必填
autoWidth: true, //非必填
bookType: 'xlsx' //非必填
})
})
excel导出参数
vue-element-admin提供了导出的功能模块,在课程资源/excel导出目录下,放置到src目录下
参数:
参数 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
header | 导出数据的表头 | Array | / | [] |
data | 导出的具体数据 | Array | / | [[]] |
filename | 导出文件名 | String | / | excel-list |
autoWidth | 单元格是否要自适应宽度 | Boolean | true / false | true |
bookType | 导出文件类型 | String | xlsx, csv, txt, more | xlsx |
excel导出基本的结构
下面代码会用到 Export2Excel.js 模块,所以首先在 src 目录下新建 vendor 目录,其中新建 Export2Excel.js ,输入如下代码
/* eslint-disable */
import { saveAs } from 'file-saver'
import XLSX from 'xlsx'
function generateArray(table) {
var out = [];
var rows = table.querySelectorAll('tr');
var ranges = [];
for (var R = 0; R < rows.length; ++R) {
var outRow = [];
var row = rows[R];
var columns = row.querySelectorAll('td');
for (var C = 0; C < columns.length; ++C) {
var cell = columns[C];
var colspan = cell.getAttribute('colspan');
var rowspan = cell.getAttribute('rowspan');
var cellValue = cell.innerText;
if (cellValue !== "" && cellValue == +cellValue) cellValue = +cellValue;
//Skip ranges
ranges.forEach(function (range) {
if (R >= range.s.r && R <= range.e.r && outRow.length >= range.s.c && outRow.length <= range.e.c) {
for (var i = 0; i <= range.e.c - range.s.c; ++i) outRow.push(null);
}
});
//Handle Row Span
if (rowspan || colspan) {
rowspan = rowspan || 1;
colspan = colspan || 1;
ranges.push({
s: {
r: R,
c: outRow.length
},
e: {
r: R + rowspan - 1,
c: outRow.length + colspan - 1
}
});
};
//Handle Value
outRow.push(cellValue !== "" ? cellValue : null);
//Handle Colspan
if (colspan)
for (var k = 0; k < colspan - 1; ++k) outRow.push(null);
}
out.push(outRow);
}
return [out, ranges];
};
function datenum(v, date1904) {
if (date1904) v += 1462;
var epoch = Date.parse(v);
return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000);
}
function sheet_from_array_of_arrays(data, opts) {
var ws = {};
var range = {
s: {
c: 10000000,
r: 10000000
},
e: {
c: 0,
r: 0
}
};
for (var R = 0; R != data.length; ++R) {
for (var C = 0; C != data[R].length; ++C) {
if (range.s.r > R) range.s.r = R;
if (range.s.c > C) range.s.c = C;
if (range.e.r < R) range.e.r = R;
if (range.e.c < C) range.e.c = C;
var cell = {
v: data[R][C]
};
if (cell.v == null) continue;
var cell_ref = XLSX.utils.encode_cell({
c: C,
r: R
});
if (typeof cell.v === 'number') cell.t = 'n';
else if (typeof cell.v === 'boolean') cell.t = 'b';
else if (cell.v instanceof Date) {
cell.t = 'n';
cell.z = XLSX.SSF._table[14];
cell.v = datenum(cell.v);
} else cell.t = 's';
ws[cell_ref] = cell;
}
}
if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range);
return ws;
}
function Workbook() {
if (!(this instanceof Workbook)) return new Workbook();
this.SheetNames = [];
this.Sheets = {};
}
function s2ab(s) {
var buf = new ArrayBuffer(s.length);
var view = new Uint8Array(buf);
for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;
return buf;
}
export function export_table_to_excel(id) {
var theTable = document.getElementById(id);
var oo = generateArray(theTable);
var ranges = oo[1];
/* original data */
var data = oo[0];
var ws_name = "SheetJS";
var wb = new Workbook(),
ws = sheet_from_array_of_arrays(data);
/* add ranges to worksheet */
// ws['!cols'] = ['apple', 'banan'];
ws['!merges'] = ranges;
/* add worksheet to workbook */
wb.SheetNames.push(ws_name);
wb.Sheets[ws_name] = ws;
var wbout = XLSX.write(wb, {
bookType: 'xlsx',
bookSST: false,
type: 'binary'
});
saveAs(new Blob([s2ab(wbout)], {
type: "application/octet-stream"
}), "test.xlsx")
}
export function export_json_to_excel({
multiHeader = [],
header,
data,
filename,
merges = [],
autoWidth = true,
bookType = 'xlsx'
} = {}) {
/* original data */
filename = filename || 'excel-list'
data = [...data]
data.unshift(header);
for (let i = multiHeader.length - 1; i > -1; i--) {
data.unshift(multiHeader[i])
}
var ws_name = "SheetJS";
var wb = new Workbook(),
ws = sheet_from_array_of_arrays(data);
if (merges.length > 0) {
if (!ws['!merges']) ws['!merges'] = [];
merges.forEach(item => {
ws['!merges'].push(XLSX.utils.decode_range(item))
})
}
if (autoWidth) {
/*设置worksheet每列的最大宽度*/
const colWidth = data.map(row => row.map(val => {
/*先判断是否为null/undefined*/
if (val == null) {
return {
'wch': 10
};
}
/*再判断是否为中文*/
else if (val.toString().charCodeAt(0) > 255) {
return {
'wch': val.toString().length * 2
};
} else {
return {
'wch': val.toString().length
};
}
}))
/*以第一行为初始值*/
let result = colWidth[0];
for (let i = 1; i < colWidth.length; i++) {
for (let j = 0; j < colWidth[i].length; j++) {
if (result[j]['wch'] < colWidth[i][j]['wch']) {
result[j]['wch'] = colWidth[i][j]['wch'];
}
}
}
ws['!cols'] = result;
}
/* add worksheet to workbook */
wb.SheetNames.push(ws_name);
wb.Sheets[ws_name] = ws;
var wbout = XLSX.write(wb, {
bookType: bookType,
bookSST: false,
type: 'binary'
});
saveAs(new Blob([s2ab(wbout)], {
type: "application/octet-stream"
}), `${filename}.${bookType}`);
}
然后实现数据的导出
我们最重要的一件事,就是把表头和数据进行相应的对应
因为数据中的key是英文,想要导出的表头是中文的话,需要将中文和英文做对应
const headers = {
'入职日期': 'create_time',
'手机号': 'mobile',
'用户名': 'username',
'角色': 'role_name',
'邮箱': 'email',
'部门': 'department_name'
}
然后,完成导出代码
import { formatDate } from '@/filters'
// 导出数据
exportData() {
const headers = {
'入职日期': 'create_time',
'手机号': 'mobile',
'用户名': 'username',
'角色': 'role_name',
'邮箱': 'email',
'部门': 'department_name'
}
import('@/vendor/Export2Excel').then(async excel => {
const res = await getUserList({ query: '', pagenum: 1, pagesize: this.page.total })
// console.log(res)
const data = this.formatJson(headers, res.users)
console.log(data)
excel.export_json_to_excel({
header: Object.keys(headers),
data,
filename: '用户信息表',
autoWidth: true,
bookType: 'xlsx'
})
})
}
}
导出时间格式处理
// 该方法负责将数组转化成二维数组
formatJson(headers, rows) {
return rows.map(item => {
return Object.keys(headers).map(key => {
if (headers[key] === 'create_time') {
return formatDate(item[headers[key]]) // formatDate 函数是定义好的一个过滤器
}
return item[headers[key]]
})
})
过滤器 formatDate
import moment from 'moment'
export function formatTime(value) {
return moment(value * 1000).format('YYYY-MM-DD HH:mm:ss')
}
export function formatDate(value) {
return moment(value * 1000).format('YYYY-MM-DD')
}
复杂标头的导出
当需要导出复杂表头的时候,vue-element-admin同样支持该类操作
vue-element-admin 提供的导出方法中有 multiHeader和merges 的参数
multiHeader里面是一个二维数组,里面的一个元素是一行表头,假设你想得到一个如图的结构
mutiHeader应该这样定义
const multiHeader = [['姓名', '主要信息', '', '', '', '', '部门']]
multiHeader中的一行表头中的字段的个数需要和真正的列数相等,假设想要跨列,多余的空间需要定义成空串
它主要对应的是标准的表头
const header = ['姓名', '手机号', '入职日期', '聘用形式', '转正日期', '工号', '部门']
如果,我们要实现其合并的效果, 需要设定merges选项
const merges = ['A1:A2', 'B1:F1', 'G1:G2']
merges的顺序是没关系的,只要配置这两个属性,就可以导出复杂表头的excel了
exportData() {
const headers = {
'入职日期': 'create_time',
'手机号': 'mobile',
'用户名': 'username',
'密码': 'password',
'邮箱': 'email',
'部门':'部门'
}
// 导出excel
import('@/vendor/Export2Excel').then(async excel => {
// excel是引入文件的导出对象
// 导出 header从哪里来
// data从哪里来
// 现在没有一个接口获取所有的数据
// 获取用户的接口 页码 每页条数 100 1 10000
const { rows } = await getEmployeeList({ page: 1, size: this.page.total })
const data = this.formatJson(headers, rows) // 返回的data就是 要导出的结构
const multiHeader = [['姓名', '主要信息', '', '', '', '', '部门']]
const merges = ['A1:A2', 'B1:F1', 'G1:G2']
excel.export_json_to_excel({
header: Object.keys(headers),
data,
filename: '用户资料表',
multiHeader, // 复杂表头
merges // 合并选项
})
// excel.export_json_to_excel({
// header: ['姓名', '工资'],
// data: [['张三', 3000], ['李四', 5000]],
// filename: '用户工资表'
// })
// [{ username: '张三',mobile: 13112345678 }] => [[]]
// 要转化 数据结构 还要和表头的顺序对应上
// 要求转出的标题是中文
})
},
// 将表头数据和数据进行对应
// [{}] => [[]]
formatJson(headers, rows) {
return rows.map(item => {
// item是一个对象 { mobile: 132111,username: '张三' }
// ["手机号", "姓名", "入职日期" 。。]
return Object.keys(headers).map(key => {
// 需要判断 字段
if (headers[key] === 'timeOfEntry' || headers[key] === 'correctionTime') {
// 格式化日期
return formatDate(item[headers[key]])
} else if (headers[key] === 'formOfEmployment') {
const obj = EmployeeEnum.hireType.find(obj => obj.id === item[headers[key]])
return obj ? obj.value : '未知'
}
return item[headers[key]]
})
// ["132", '张三’, ‘’,‘’,‘’d]
})
// return rows.map(item => Object.keys(headers).map(key => item[headers[key]]))
// 需要处理时间格式问题
}
过滤器
过滤就是一个数据经过了过滤之后出来另一样东西,可以从中取得你想要的,也可以给数据添加筛选,过滤器就是过滤的工具
标题过滤器时间格式的处理
<el-table-column label="入职时间" sortable prop="timeOfEntry">
<template slot-scope="obj">
{{
obj.row.timeOfEntry | 过滤器
}}
</template>
</el-table-column>
src 目录下新建 filters 目录,其中新建 index.js 文件
这里我们使用 moment 做时间的格式化,所以需要首先安装 moment
npm i moment
然后编写过滤器函数
import moment from 'moment'
export function formatTime(value) {
return moment(value * 1000).format('YYYY-MM-DD HH:mm:ss')
}
在main.js中全局注册过滤器
import * as filters from '@/filters'
Object.keys(filters).forEach(key => {
Vue.filter(key, filters[key])
})
自定义指令
除了核心功能默认内置的指令 (v-model 和 v-show),Vue 也允许注册自定义指令
使用 Vue.directive(id, [definition]) 定义全局的指令来进行自定义指令
参数1 : 指令的名称,注意,在定义的时候,指令的名称前面,不需要加 v-前缀,但是: 在调用的时候,必须在置顶的名称前加上 v-前缀来进行调用
参数2: 是一个对象, 这个对象身上,有一些指令相关的函数,这些函数可以在特定的阶段,执行相关的操作
Vue.directive("focus", {
// 注意:在每个函数中, 第一个参数永远是el, 表示被绑定了指令的那个元素,这个el参数,是一个原生的JS对象
bind: function(el){ // 每当指令绑定到元素上的时候,会立即执行这个bind函数,只执行一次
},
inserted: function(el){ // inserted 表示元素插入到DOM中的时候,会执行inserted函数【触发一次】
el.focus()
},
updated: function(el) { // 当VNode更新的时候,会执行updated,可能会触发多次
},
})
调用
<!-- 注意: Vue中所有的指令,在调用的时候,都以 v- 开头 -->
<input type="text" class="form-control" v-model="keywords" v-focus>
钩子函数
一个指令定义对象可以提供如下几个钩子函数 (均为可选):
bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是
你可以通过比较更新前后的值来忽略不必要的模板更新
componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
unbind:只调用一次,指令与元素解绑时调用
用户头像上传
新建文件上传组件
安装javascript SDK
$ npm i cos-js-sdk-v5 --save
新建上传图片组件 src/components/ImageUpload/index.vue
放置el-upload组件
<template>
<el-upload list-type="picture-card">
<i class="el-icon-plus" />
</el-upload>
</template>
全局注册组件
import ImageUpload from './ImageUpload'
export default {
install(Vue) {
Vue.component('ImageUpload', ImageUpload) // 注册导入上传组件
}
}
点击图片预览
限定上传的图片数量和action
<template>
<el-upload list-type="picture-card" :limit="1" action="#">
</el-upload>
</template>
action为什么给#,需要自定义的上传方式,action给个#防止报错
预览
data() {
return {
fileList: [], // 图片地址设置为数组
showDialog: false, // 控制显示弹层
imgUrl: ''
}
},
preview(file) {
// 这里应该弹出一个层 层里是点击的图片地址
this.imgUrl = file.url
this.showDialog = true
},
预览弹层
<el-dialog title="图片" :visible.sync="showDialog">
<img :src="imgUrl" style="width:100%" alt="">
</el-dialog>
limit属性可以限制上传的图片数量
添加,删除 图片
changeFile(file, fileList) {
this.fileList = fileList.map(item => item)
}
handleRemove(file) {
// file是点击删除的文件
// 将原来的文件给排除掉了 剩下的就是最新的数组了
this.fileList = this.fileList.filter(item => item.uid !== file.uid)
},
上传之前检查
控制上传图片的类型和上传大小, 如果不满足条件 返回false上传就会停止
beforeUpload(file) {
// 要开始做文件上传的检查了
// 文件类型 文件大小
const types = ['image/jpeg', 'image/gif', 'image/bmp', 'image/png']
if (!types.includes(file.type)) {
this.$message.error('上传图片只能是 JPG、GIF、BMP、PNG 格式!')
return false
}
// 检查大小
const maxSize = 5 * 1024 * 1024
if (maxSize < file.size) {
this.$message.error('图片大小最大不能超过5M')
return false
}
return true
}
上传成功至腾讯云存储桶
upload (params) {
this.currentImageUid = params.file.uid
// console.log(params.file)
if (params.file) {
// 执行上传操作
cos.putObject({
Bucket: 'zzstore-1305896542', // 存储桶
Region: 'ap-nanjing', // 地域
Key: params.file.name, // 文件名
Body: params.file, // 要上传的文件对象
StorageClass: 'STANDARD',// 上传的模式类型 直接默认 标准模式即可
onProgress: (progressData) => {
this.percent = progressData.percent * 100
}
// 上传到腾讯云 =》 哪个存储桶 哪个地域的存储桶 文件 格式 名称 回调
}, (err, data) => {
// data返回数据之后 应该如何处理
// console.log(err || data)
this.fileList = this.fileList.map(item => {
if (item.uid === this.currentImageUid) {
return { url: 'http://' data.Location, name: item.name }
}
return item
})
this.showPercent = false
})
}
},
RBAC权限模式
用户对应相应的角色,角色对应相应的权限
用户可以有多种角色,角色也可以多种权限
前端权限控制
在权限管理页面中,我们设置了一个标识, 这个标识可以和我们的路由模块进行关联,也就是说,如果用户拥有这个标识,那么用户就可以拥有这个路由模块,如果没有这个标识,就不能访问路由模块vue-router提供了一个叫做addRoutes的API方法,这个方法的含义是动态添加路由规则
创建vue实例的时候将vue-router挂载,这是就有静态权限,当用户点击登录后获取role,将role和路由种的每个页面的权限进行比较,在显示页面侧边栏中的就是用户自带的权限
路由代码
路由守卫
国际化
引入国际包
npm i vue-i18n
在mian.js种挂载i18n的插件并设置element为当前的语言
引入自定义语言包
在左侧菜单应用
当我们全局注册i18n的时候,每个组件都会拥有一个**$t**的方法,它会根据传入的key,自动的去寻找当前语言的文本,我们可以将左侧菜单变成多语言展示文本
layout/components/Sidebar/SidebarItem.vue
注意
子路由和父级路由的name值不要相同,控制台会显示路由名字重复的提示
将页面中的数据进行国际化
封装多语言插件
在Navbar组件中引入
以上是关于vue-element-admin的主要内容,如果未能解决你的问题,请参考以下文章