本节任务
:完成业务路由页面的整理
目标
: 快速搭建人资项目的常规业务模块
截止到现在,我们已经完成了一个中台系统的基本轮廓,如图
接下来,我们可以将人力资源需要做的模块快速搭建相应的页面和路由
├── dashboard # 首页
├── login # 登录
├── 404 # 404
├── departments # 组织架构
├── employees # 员工
├── setting # 公司设置
├── salarys # 工资
├── social # 社保
├── attendances # 考勤
├── approvals # 审批
├── permission # 权限管理
根据上图中的结构,在views目录下,建立对应的目录,给每个模块新建一个**index.vue
**,作为每个模块的主页
快速新建文件夹
$ mkdir departments employees setting salarys social attendances approvals permission
每个模块的内容,可以先按照标准的模板建立,如
员工
员工
根据以上的标准建立好对应页面之后,接下来建立每个模块的路由规则
路由模块目录结构
├── router # 路由目录
├── index.js # 路由主文件
├── modules # 模块目录
├── departments.js # 组织架构
├── employees.js # 员工
├── setting.js # 公司设置
├── salarys.js # 工资
├── social.js # 社保
├── attendances.js # 考勤
├── approvals.js # 审批
├── permission.js # 权限管理
快速创建命令
$ touch departments.js employees.js setting.js salarys.js salarys.js social.js attendances.js approvals.js permission.js
每个模块导出的内容表示该模块下的路由规则
如员工 employees.js
// 导出属于员工的路由规则
import Layout from '@/layout'
// { path: '', component: '' }
// 每个子模块 其实 都是外层是layout 组件位于layout的二级路由里面
export default {
path: '/employees', // 路径
name: 'employees', // 给路由规则加一个name
component: Layout, // 组件
// 配置二级路的路由表
children: [{
path: '', // 这里当二级路由的path什么都不写的时候 表示该路由为当前二级路由的默认路由
component: () => import('@/views/employees'),
// 路由元信息 其实就是存储数据的对象 我们可以在这里放置一些信息
meta: {
title: '员工管理' // meta属性的里面的属性 随意定义 但是这里为什么要用title呢, 因为左侧导航会读取我们的路由里的meta里面的title作为显示菜单名称
}
}]
}
// 当你的访问地址 是 /employees的时候 layout组件会显示 此时 你的二级路由的默认组件 也会显示
上述代码中,我们用到了meta属性,该属性为一个对象,里面可放置自定义属性,主要用于读取一些配置和参数,并且值得**
注意
的是:我们的meta写了二级默认路由上面,而不是一级路由,因为当存在二级路由的时候,访问当前路由信息访问的就是二级默认路由
**
大家针对上述的设计,对上面的模块进行快速的搭建
提交代码
本节任务
:完成其他模块的页面和路由的快速搭建
目标
: 将静态路由和动态路由的路由表进行临时合并
什么叫临时合并?
在第一个小节中,我们讲过了,动态路由是需要权限进行访问的,但是权限的动态路由访问是很复杂的,我们稍后在进行讲解,所以为了更好地看到效果,我们可以先将 静态路由和动态路由进行合并
路由主文件 src/router/index.js
// 引入多个模块的规则
import approvalsRouter from './modules/approvals'
import departmentsRouter from './modules/departments'
import employeesRouter from './modules/employees'
import permissionRouter from './modules/permission'
import attendancesRouter from './modules/attendances'
import salarysRouter from './modules/salarys'
import settingRouter from './modules/setting'
import socialRouter from './modules/social'
// 动态路由
export const asyncRoutes = [
approvalsRouter,
departmentsRouter,
employeesRouter,
permissionRouter,
attendancesRouter,
salarysRouter,
settingRouter,
socialRouter
]
const createRouter = () => new Router({
// mode: 'history', // require service support
scrollBehavior: () => ({
y: 0 }), // 管理滚动行为 如果出现滚动 切换就让 让页面回到顶部
routes: [...constantRoutes, ...asyncRoutes] // 临时合并所有的路由
})
通过上面的操作,我们将静态路由和动态路由进行了合并
当我们合并权限完成,我们惊奇的发现页面效果已经左侧的导航菜单 =》 路由页面
这是之前基础模板中对于左侧导航菜单的封装
提交代码
本节任务
: 将静态路由和动态路由临时合并,形成左侧菜单
目标
解析左侧菜单的显示逻辑, 设置左侧导航菜单的图标内容
上小节中,我们集成了路由,菜单就显示内容了,这是为什么 ?
阅读左侧菜单代码
我们发现如图的逻辑
由于,该项目不需要二级菜单的显示,所以对代码进行一下处理,只保留一级菜单路由
src/layout/components/Sidebar/SidebarItem.vue
本节注意
:通过代码发现,当路由中的属性**hidden
**为true时,表示该路由不显示在左侧菜单中
与此同时,我们发现左侧菜单并不协调,是因为缺少图标。在本项目中,我们的图标采用了SVG的组件
左侧菜单的图标实际上读取的是meta属性的icon,这个icon需要我们提前放置在**src/icons/svg
**目录下
该资源已经在菜单svg目录中提供,请将该目录下的所有svg放到**
src/icons/svg
**目录下
具体的icon名称可参考线上地址
functional为true,表示该组件为一个函数式组件
函数式组件: 没有data状态,没有响应式数据,只会接收props属性, 没有this, 他就是一个函数
模块对应icon
├── dashboard # dashboard
├── departments # tree
├── employees # people
├── setting # setting
├── salarys # money
├── social # table
├── attendances # skill
├── approvals # tree-table
├── permission # lock
按照对应的icon设置图标
本节任务:
理解左侧菜单的生成逻辑,并设置左侧菜单的图标
目标
:使用element-UI组件布局组织架构的基本布局
组织架构产品prd
一个企业的组织架构是该企业的灵魂,组织架构多常采用树形金字塔式结构,本章节,我们布局出页面的基本结构
首先实现头部的结构,采用element的行列布局
江苏传智播客教育科技股份有限公司
负责人
操作
添加子部门
样式
接下来,实现树形的结构,采用element的**tree组件**, 如图效果
树形组件属性
参数 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
default-expand-all | 是否默认展开所有节点 | boolean | — | — |
data | 展示数据 | array | — | — |
node-key | 每个树节点用来作为唯一标识的属性,整棵树应该是唯一的 | String | — | — |
props | 配置选项,具体看下表 | object | — | — |
props属性
参数 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
label | 指定节点标签为节点对象的某个属性值 | string, function(data, node) | — | — |
children | 指定子树为节点对象的某个属性值 | string | — | — |
disabled | 指定节点选择框是否禁用为节点对象的某个属性值 | boolean, function(data, node) | — | — |
isLeaf | 指定节点是否为叶子节点,仅在指定了 lazy 属性的情况下生效 | boolean, function(data, node) | — | — |
data是组成树形数据的关键,如下的数据便能构建树形数据
[{
label: '一级 1',
children: [{
label: '二级 1-1',
children: [{
label: '三级 1-1-1'
}]
}]
}, {
label: '一级 2',
children: [{
label: '二级 2-1',
children: [{
label: '三级 2-1-1'
}]
}, {
label: '二级 2-2',
children: [{
label: '三级 2-2-1'
}]
}]
}, {
label: '一级 3',
children: [{
label: '二级 3-1',
children: [{
label: '三级 3-1-1'
}]
}, {
label: '二级 3-2',
children: [{
label: '三级 3-2-1'
}]
}]
}]
由此,我们首先实现静态数据的组织架构
export default {
data() {
return {
defaultProps: {
label: 'name'
},
departs: [{ name: '总裁办', children: [{ name: '董事会' }] },
{ name: '行政部' }, { name: '人事部' }]
}
}
}
接下来,对每个层级节点增加显示内容,此时需要用到tree的插槽
{
{ data.name }}
{
{ data.manager }}
操作
添加子部门
编辑部门
删除部门
最终形成静态结构效果
提交代码
本节任务
:完成树形结构的显示
目标
: 将树形的操作内容单独抽提成组件
通过第一个章节,我们发现,树形的顶级内容实际和子节点的内容是一致的,此时可以将该部分抽提成一个组件,节省代码
组件 src/views/departments/components/tree-tools.vue
{
{ treeNode.name }}
{
{ treeNode.manager }}
操作
添加子部门
编辑部门
删除部门
接下来,在**src/views/departments/index.vue
**进行代码的简化
上面代码中,company变量需要在data中定义
company: {
name: '江苏传智播客教育科技股份有限公司', manager: '负责人' },
同时,由于在两个位置都使用了该组件,但是放置在最上层的组件是不需要显示 **删除部门
和编辑部门
**的
所以,增加一个新的属性 **isRoot(是否根节点)
**进行控制
props: {
treeNode: {
required: true, // 设置当前数据为必填
type: Object // 类型是Object
},
isRoot: {
type: Boolean,
default: false
}
}
<tree-tools :tree-node="company" :is-root="true" />
组件中, 根据isRoot判断显示
编辑部门
删除部门
通过封装,代码看上去更加紧凑,简洁,这就是封装的魅力
提交代码
本节任务
:将树形内容单独抽提组件
**目标
**获取真实的组织架构数据,并将其转化成树形数据显示在页面上
现在基本的静态结构已经形成,接下来需要获取真实的数据
首先,封装获取组织架构的请求 src/api/departments.js
/** *
*
* 获取组织架构数据
* **/
export function getDepartments() {
return request({
url: '/company/department'
})
}
在钩子函数中调用接口
import TreeTools from './components/tree-tools'
import {
getDepartments } from '@/api/departments'
export default {
components: {
TreeTools
},
data() {
return {
company: {
}, // 就是头部的数据结构
departs: [],
defaultProps: {
label: 'name' // 表示 从这个属性显示内容
}
}
},
created() {
this.getDepartments() // 调用自身的方法
},
methods: {
async getDepartments() {
const result = await getDepartments()
this.company = {
name: result.companyName, manager: '负责人' }
this.departs = result.depts // 需要将其转化成树形结构
console.log(result)
}
}
}
然后,我们需要将列表型的数据,转化成树形数据,这里需要用到递归算法
封装一个工具方法,src/utils/index.js
/** *
*
* 将列表型的数据转化成树形数据 => 递归算法 => 自身调用自身 => 一定条件不能一样, 否则就会死循环
* 遍历树形 有一个重点 要先找一个头儿
* ***/
export function tranListToTreeData(list, rootValue) {
var arr = []
list.forEach(item => {
if (item.pid === rootValue) {
// 找到之后 就要去找 item 下面有没有子节点
const children = tranListToTreeData(list, item.id)
if (children.length) {
// 如果children的长度大于0 说明找到了子节点
item.children = children
}
arr.push(item) // 将内容加入到数组中
}
})
return arr
}
调用转化方法,转化树形结构
this.company = {
name: result.companyName, manager: '负责人' } // 这里定义一个空串 因为 它是根 所有的子节点的数据pid 都是 ""
this.departs = transListToTreeData(result.depts, '')
这样一来,树形数据就有了,下一章节,就可以针对部门进行操作
提交代码
本节任务
获取组织架构数据,并进行树形处理
**目标
**实现操作功能的删除功能
首先,封装删除功能模块 src/api/departments.js
/** *
* 根据id根据部门 接口是根据restful的规则设计的 删除 delete 新增 post 修改put 获取 get
* **/
export function delDepartments(id) {
return request({
url: `/company/department/${
id}`,
method: 'delete'
})
}
然后,在tree-tools组件中,监听下拉菜单的点击事件 src/views/departments/index.vue
操作
添加子部门
编辑部门
删除部门
dropdown下拉菜单的监听事件command
// 操作节点调用的方法
operateDepts(type) {
if (type === 'add') {
// 添加子部门的操作
} else if (type === 'edit') {
// 编辑部门的操作
} else {
// 删除操作
}
}
删除之前,提示用户是否删除,然后调用删除接口
// 操作节点调用的方法
operateDepts(type) {
if (type === 'add') {
// 添加子部门的操作
} else if (type === 'edit') {
// 编辑部门的操作
} else {
// 删除操作
this.$confirm('确定要删除该部门吗').then(() => {
// 如果点击了确定就会进入then
return delDepartments(this.treeNode.id) // 返回promise对象
}).then(() => {
// 如果删除成功了 就会进入这里
})
}
}
上面代码中,我们已经成功删除了员工数据,但是怎么通知父组件进行更新呢
在前面的课程中,我们已经学习过可以通过自定义事件**this.$emit
**的方式来进行
// 如果删除成功了 就会进入这里
this.$emit('delDepts') // 触发自定义事件
this.$message.success('删除部门成功')
父组件监听事件 src/views/department/index.vue
提交代码
本节任务
:删除部门功能实现
目标
:实现新增部门功能的组件建立
首先, 封装新增部门的api模块 src/api/departments.js
/**
* 新增部门接口
*
* ****/
export function addDepartments(data) {
return request({
url: '/company/department',
method: 'post',
data
})
}
然后,我们需要构建一个新增部门的窗体组件 src/views/department/components/add-dept.vue
其中的交互设计如下
设计要求
确定
取消
然后,我们需要用属性控制组件的显示或者隐藏
// 需要传入一个props变量来控制 显示或者隐藏
props: {
showDialog: {
type: Boolean,
default: false
}
}
在**departments/index.vue
** 中引入该组件
import AddDept from './components/add-dept' // 引入新增部门组件
export default {
components: {
AddDept }
}
定义控制窗体显示的变量**showDialog
**
data() {
return {
showDialog: false // 显示窗体
}
},
<!-- 放置新增弹层组件 -->
<add-dept :show-dialog="showDialog" />
当点击新增部门时,弹出组件
注意,点击新增时tree-tools组件,所以这里,我们依然需要子组件调用父组件
子组件触发新增事件· src/views/departments/tree-tools.vue
if (type === 'add') {
// 添加子部门的操作
// 告诉父组件 显示弹层
this.$emit('addDepts', this.treeNode) // 为何传出treeNode 因为是添加子部门 需要当前部门的数据
}
父组件监听事件
方法中弹出层,记录在哪个节点下添加子部门
addDepts(node) {
this.showDialog = true // 显示弹层
// 因为node是当前的点击的部门, 此时这个部门应该记录下来,
this.node = node
}
提交代码
本节任务
:新增部门功能-建立组件
目标
完成新增部门功能的规则校验和数据提交部分
部门名称(name):必填 1-50个字符 / 同级部门中禁止出现重复部门
部门编码(code):必填 1-50个字符 / 部门编码在整个模块中都不允许重复
部门负责人(manager):必填
部门介绍 ( introduce):必填 1-300个字符
定义数据结构
formData: {
name: '', // 部门名称
code: '', // 部门编码
manager: '', // 部门管理者
introduce: '' // 部门介绍
},
完成表单校验需要的前置条件
根据这些要求,校验规则
data() {
return {
// 定义表单数据
formData: {
name: '', // 部门名称
code: '', // 部门编码
manager: '', // 部门管理者
introduce: '' // 部门介绍
},
// 定义校验规则
rules: {
name: [{
required: true, message: '部门名称不能为空', trigger: 'blur' },
{
min: 1, max: 50, message: '部门名称要求1-50个字符', trigger: 'blur' }],
code: [{
required: true, message: '部门编码不能为空', trigger: 'blur' },
{
min: 1, max: 50, message: '部门编码要求1-50个字符', trigger: 'blur' }],
manager: [{
required: true, message: '部门负责人不能为空', trigger: 'blur' }],
introduce: [{
required: true, message: '部门介绍不能为空', trigger: 'blur' },
{
trigger: 'blur', min: 1, max: 300, message: '部门介绍要求1-50个字符' }]
}
}
}
注意
:部门名称和部门编码的规则 有两条我们需要通过**自定义校验函数validator
**来实现
首先,在校验名称和编码时,要获取最新的组织架构,这也是我们这里trigger采用blur的原因,因为change对于访问的频率过高,我们需要控制访问频率
// 首先获取最新的组织架构数据
const {
depts } = await getDepartments()
部门名称不能和**
同级别
**的重复,这里注意,我们需要找到所有同级别的数据,进行校验,所以还需要另一个参数pid
props: {
// 用来控制窗体是否显示或者隐藏
showDialog: {
type: Boolean,
default: false
},
// 当前操作的节点
treeNode: {
type: Object,
default: null
}
},
<add-dept :show-dialog="showDialog" :tree-node="node" />
根据当前部门id,找到所有子部门相关的数据,判断是否重复
// 现在定义一个函数 这个函数的目的是 去找 同级部门下 是否有重复的部门名称
const checkNameRepeat = async(rule, value, callback) => {
// 先要获取最新的组织架构数据
const {
depts } = await getDepartments()
// depts是所有的部门数据
// 如何去找技术部所有的子节点
const isRepeat = depts.filter(item => item.pid === this.treeNode.id).some(item => item.name === value)
isRepeat ? callback(new Error(`同级部门下已经有${
value}的部门了`)) : callback()
}
检查部门编码的过程同理
// 检查编码重复
const checkCodeRepeat = async(rule, value, callback) => {
// 先要获取最新的组织架构数据
const {
depts } = await getDepartments()
const isRepeat = depts.some(item => item.code === value && value) // 这里加一个 value不为空 因为我们的部门有可能没有code
isRepeat ? callback(new Error(`组织架构中已经有部门使用${
value}编码`)) : callback()
}
在规则中定义
// 定义校验规则
rules: {
name: [{
required: true, message: '部门名称不能为空', trigger: 'blur' },
{
min: 1, max: 50, message: '部门名称要求1-50个字符', trigger: 'blur' }, {
trigger: 'blur',
validator: checkNameRepeat // 自定义函数的形式校验
}],
code: [{
required: true, message: '部门编码不能为空', trigger: 'blur' },
{
min: 1, max: 50, message: '部门编码要求1-50个字符', trigger: 'blur' }, {
trigger: 'blur',
validator: checkCodeRepeat
}],
manager: [{
required: true, message: '部门负责人不能为空', trigger: 'blur' }],
introduce: [{
required: true, message: '部门介绍不能为空', trigger: 'blur' },
{
trigger: 'blur', min: 1, max: 300, message: '部门介绍要求1-50个字符' }]
}
需要注意
:在最根级的**tree-tools
**组件中,由于treenode属性中没有id,id便是undefined,但是通过undefined进行等值判断是寻找不到对应的根节点的, 所以在传值时,我们将id属性设置为 “”
src/views/departments/index.vue
async getDepartments() {
const result = await getDepartments()
this.departs = transListToTreeData(result.depts, '')
this.company = {
name: result.companyName, manager: '负责人', id: '' }
},
提交代码
本节任务
:完成新增部门的规则校验
目标
:获取新增表单中的部门负责人下拉数据
在上节的表单中,部门负责人是下拉数据,我们应该从**
员工接口
**中获取该数据
首先,封装获取简单员工列表的模块 src/api/employees.js
import request from '@/utils/request'
/**
* 获取员工的简单列表
* **/
export function getEmployeeSimple() {
return request({
url: '/sys/user/simple'
})
}
然后,在**add-dept.vue
中的select聚焦事件focus
**中调用该接口,因为我们要获取实时的最新数据
获取员工列表
import {
getEmployeeSimple } from '@/api/employees'
methods: {
// 获取员工简单列表数据
async getEmployeeSimple() {
this.peoples = await getEmployeeSimple()
}
}
peoples: [] // 接收获取的员工简单列表的数据
提交代码
本节任务
:新增部门功能-部门负责人数据
目标
: 完成新增模块的提交-取消-关闭等功能
当点击新增页面的确定按钮时,我们需要完成对表单的整体校验,如果校验成功,进行提交
首先,在点击确定时,校验表单
给el-form定义一个ref属性
// 点击确定时触发
btnOK() {
this.$refs.deptForm.validate(isOK => {
if (isOK) {
// 表示可以提交了
}
})
}
然后,在校验通过时,调用新增接口
因为是添加子部门,所以我们需要将新增的部门pid设置成当前部门的id,新增的部门就成了自己的子部门
// 点击确定时触发
btnOK() {
this.$refs.deptForm.validate(async isOK => {
if (isOK) {
// 表示可以提交了
await addDepartments({
...this.formData, pid: this.treeNode.id }) // 调用新增接口 添加父部门的id
}
})
}
同样,在新增成功之后,调用告诉父组件,重新拉取数据
this.$emit('addDepts')
父组件
本节注意
:同学们可能会疑惑,我们**tree-tools.vue
** 和**add-dept.vue
**两个组件都触发了addDepts事件,不冲突吗?
这里,我们触发的自定义事件都是组件自身的,他们之间没有任何关系,只是名字相同而已,大家不要混淆
这里我们学习一个新的技巧,
sync修饰符
按照常规,想要让父组件更新**showDialog
**的话,需要这样做
// 子组件
this.$emit('changedialog', false) //触发事件
// 父组件
<child @changedialog="method" :showDialog="showDialog" />
method(value) {
this.showDialog = value
}
但是,vuejs为我们提供了**
sync修饰符
**,它提供了一种简写模式 也就是
// 子组件 update:固定写法 (update:props名称, 值)
this.$emit('update:showDialog', false) //触发事件
// 父组件 sync修饰符
<child :showDialog.sync="showDialog" />
只要用sync修饰,就可以省略父组件的监听和方法,直接将值赋值给showDialog
取消按钮和关闭
// 点击确定时触发
btnOK() {
this.$refs.deptForm.validate(async isOK => {
if (isOK) {
// 表示可以提交了
await addDepartments({
...this.formData, pid: this.treeNode.id }) // 调用新增接口 添加父部门的id
this.$emit('addDepts') // 告诉父组件 新增数据成功 重新拉取数据
// update:props名称
this.$emit('update:showDialog', false)
}
})
}
btnCancel() {
this.$refs.deptForm.resetFields() // 重置校验字段
this.$emit('update:showDialog', false) // 关闭
}
需要在el-dialog中监听其close事件
本节任务
新增功能-提交-取消-关闭
目标
:实现编辑部门的功能
编辑部门功能实际上和新增窗体采用的是一个组件,只不过我们需要将新增场景变成编辑场景
首先点击编辑部门时, 调用父组件编辑方法 tree-tools.vue
this.$emit('editDepts', this.treeNode)
父组件弹层,赋值当前编辑节点
// 编辑部门节点
editDepts(node) {
// 首先打开弹层
this.showDialog = true
this.node = node // 赋值操作的节点
}
编辑时,我们需要获取点击部门的信息
封装获取部门信息的模块 src/api/departments.js
/** *
* 获取部门详情
* ***/
export function getDepartDetail(id) {
return request({
url: `/company/department/${
id}`
})
}
在什么时候获取部门详情?
我们可以在调用编辑方法 **editDepts
中通过ref
调用add-dept.vue
**的实例方法
// 获取部门详情
async getDepartDetail(id) {
this.formData = await getDepartDetail(id)
}
// 点击编辑触发的父组件的方法
editDepts(node) {
this.showDialog = true // 显示新增组件弹层
this.node = node // 存储传递过来的node数据
// 我们需要在这个位置 调用子组件的方法
// 父组件 调用子组件的方法
this.$refs.addDept.getDepartDetail(node.id) // 直接调用子组件中的方法 传入一个id
}
需要根据当前的场景区分显示的标题
计算属性
如何判断新增还是编辑
computed: {
showTitle() {
return this.formData.id ? '编辑部门' : '新增子部门'
}
},
同时发现,el-form中的resetFields不能重置非表单中的数据,所以在取消的位置需要强制加上 重置数据
btnCancel() {
// 重置数据 因为resetFields 只能重置 表单上的数据 非表单上的 比如 编辑中id 不能重置
this.formData = {
name: '',
code: '',
manager: '',
introduce: ''
}
// 关闭弹层
this.$emit('update:showDialog', false)
// 清除之前的校验 可以重置数据 只能重置 定义在data中的数据
this.$refs.deptForm.resetFields()
}
接下来,需要在点击确定时,同时支持新增部门和编辑部门两个场景,我们可以根据formData是否有id进行区分
封装编辑部门接口 src/api/departments.js
/**
* 编辑部门
*
* ***/
export function updateDepartments(data) {
return request({
url: `/company/department/${
data.id}`,
method: 'put',
data
})
}
点击确定时,进行场景区分
// 点击确定时触发
btnOK() {
this.$refs.deptForm.validate(async isOK => {
if (isOK) {
// 要分清楚现在是编辑还是新增
if (this.formData.id) {
// 编辑模式 调用编辑接口
await updateDepartments(this.formData)
} else {
// 新增模式
await addDepartments({
...this.formData, pid: this.treeNode.id }) // 调用新增接口 添加父部门的id
}
// 表示可以提交了
this.$emit('addDepts') // 告诉父组件 新增数据成功 重新拉取数据
// update:props名称
this.$emit('update:showDialog', false)
}
})
},
除此之外,我们发现原来的校验规则实际和编辑部门有些冲突,所以需要进一步处理
// 现在定义一个函数 这个函数的目的是 去找 同级部门下 是否有重复的部门名称
const checkNameRepeat = async(rule, value, callback) => {
// 先要获取最新的组织架构数据
const {
depts } = await getDepartments()
// 检查重复规则 需要支持两种 新增模式 / 编辑模式
// depts是所有的部门数据
// 如何去找技术部所有的子节点
let isRepeat = false
if (this.formData.id) {
// 有id就是编辑模式
// 编辑 张三 => 校验规则 除了我之外 同级部门下 不能有叫张三的
isRepeat = depts.filter(item => item.id !== this.formData.id && item.pid === this.treeNode.pid).some(item => item.name === value)
} else {
// 没id就是新增模式
isRepeat = depts.filter(item => item.pid === this.treeNode.id).some(item => item.name === value)
}
isRepeat ? callback(new Error(`同级部门下已经有${
value}的部门了`)) : callback()
}
// 检查编码重复
const checkCodeRepeat = async(rule, value, callback) => {
// 先要获取最新的组织架构数据
// 检查重复规则 需要支持两种 新增模式 / 编辑模式
const {
depts } = await getDepartments()
let isRepeat = false
if (this.formData.id) {
// 编辑模式 因为编辑模式下 不能算自己
isRepeat = depts.some(item => item.id !== this.formData.id && item.code === value && value)
} else {
// 新增模式
isRepeat = depts.some(item => item.code === value && value) // 这里加一个 value不为空 因为我们的部门有可能没有code
}
isRepeat ? callback(new Error(`组织架构中已经有部门使用${
value}编码`)) : callback()
}
至此,整个组织架构, 我们完成了,组织架构读取 / 新增部门 / 删除部门 / 编辑部门
如图
提交代码
**本节任务
**编辑部门功能实现
目标
给当前组织架构添加加载进度条
由于获取数据的延迟性,为了更好的体验,可以给页面增加一个Loading进度条,采用element的指令解决方案即可
定义loading变量
loading: false // 用来控制进度弹层的显示和隐藏
赋值变量给指令
获取方法前后设置变量
async getDepartments() {
this.loading = true
const result = await getDepartments()
this.departs = transListToTreeData(result.depts, '')
this.company = {
name: result.companyName, manager: '负责人', id: '' }
this.loading = false
}
建立公司角色页面的基本结构
**目标
**建立公司页面的基本结构
根据以上的结构,我们采用element-ui的组件实现
src/views/setting/index.vue
新增角色
分配权限
编辑
删除
提交代码
**本节任务
**建立公司页面的基本结构
读取公司角色信息
目标
: 封装公司角色请求,读取公司角色信息
读取角色列表数据
首先,封装读取角色的信息的请求 src/api/setting.js
/**
* 获取角色列表
* ***/
export function getRoleList(params) {
return request({
url: '/sys/role',
params
})
}
params是查询参数,里面需要携带分页信息
然后,在页面中调用接口获取数据,绑定表格数据 src/views/setting/index.vue
import {
getRoleList } from '@/api/setting'
export default {
data() {
return {
list: [], // 承接数组
page: {
// 放置页码及相关数据
page: 1,
pagesize: 10,
total: 0 // 记录总数
}
}
},
created() {
this.getRoleList() // 获取角色列表
},
methods: {
async getRoleList() {
const {
total, rows } = await getRoleList(this.page)
this.page.total = total
this.list = rows
},
changePage(newPage) {
// newPage是当前点击的页码
this.page.page = newPage // 将当前页码赋值给当前的最新页码
this.getRoleList()
}
}
}
绑定表格数据
分配权限
编辑
删除
绑定分页数据
读取公司信息数据
第二个tab页,我们同样需要读取数据
封装读取公司信息的api src/api/setting.js
/**
* 获取公司信息
* **/
export function getCompanyInfo(companyId) {
return request({
url: `/company/${
companyId}`
})
}
绑定公司表单数据
请求中的companyId来自哪里?它来自我们登录成功之后的用户资料,所以我们需要在该组件中使用vuex数据
src/store/getters.js
companyId: state => state.user.userInfo.companyId // 建立对于user模块的companyId的快捷访问
computed: {
...mapGetters(['companyId'])
},
初始化时调用接口
// 获取的公司的信息
async getCompanyInfo() {
this.formData = await getCompanyInfo(this.companyId)
}
created() {
this.getRoleList() // 获取角色列表
this.getCompanyInfo()
},
提交代码
**本节任务
**读取公司角色信息
删除角色功能
目标
实现删除角色的功能
封装删除角色的api
/** **
* 删除角色
*
* ****/
export function deleteRole(id) {
return request({
url: `/sys/role/${
id}`,
method: 'delete'
})
}
删除功能实现
async deleteRole(id) {
// 提示
try {
await this.$confirm('确认删除该角色吗')
// 只有点击了确定 才能进入到下方
await deleteRole(id) // 调用删除接口
this.getRoleList() // 重新加载数据
this.$message.success('删除角色成功')
} catch (error) {
console.log(error)
}
}
删除按钮注册事件
分配权限
编辑
删除
提交代码
编辑角色功能
目标
: 实现编辑角色的功能
封装编辑接口,新建编辑弹层
封装编辑角色的功能api
/** *
* 修改角色
* ***/
export function updateRole(data) {
return request({
url: `/sys/role/${
data.id}`,
data,
method: 'put'
})
}
/**
* 获取角色详情
* **/
export function getRoleDetail(id) {
return request({
url: `/sys/role/${
id}`
})
}
定义编辑弹层数据
showDialog: false,
// 专门接收新增或者编辑的编辑的表单数据
roleForm: {
},
rules: {
name: [{
required: true, message: '角色名称不能为空', trigger: 'blur' }]
},
编辑弹层结构
取消
确定
实现编辑功能,为新增功能留口
编辑功能实现(为新增功能留口)
async editRole(id) {
this.roleForm = await getRoleDetail(id)
this.showDialog = true // 为了不出现闪烁的问题 先获取数据 再弹出层
},
async btnOK() {
try {
await this.$refs.roleForm.validate()
// 只有校验通过的情况下 才会执行await的下方内容
// roleForm这个对象有id就是编辑 没有id就是新增
if (this.roleForm.id) {
await updateRole(this.roleForm)
} else {
// 新增业务
}
// 重新拉取数据
this.getRoleList()
this.$message.success('操作成功')
this.showDialog = false
} catch (error) {
console.log(error)
}
},
编辑按钮注册事件
编辑
提交代码
新增角色功能
**目标
**实现新增角色功能
封装新增角色功能api
/** *
* 新增角色
* ***/
export function addRole(data) {
return request({
url: '/sys/role',
data,
method: 'post'
})
}
新增功能实现和编辑功能合并(处理关闭)
async btnOK() {
try {
await this.$refs.roleForm.validate()
// 只有校验通过的情况下 才会执行await的下方内容
// roleForm这个对象有id就是编辑 没有id就是新增
if (this.roleForm.id) {
await updateRole(this.roleForm)
} else {
// 新增业务
await addRole(this.roleForm)
}
// 重新拉取数据
this.getRoleList()
this.$message.success('操作成功')
this.showDialog = false // 关闭弹层 => 触发el-dialog的事件close事件
} catch (error) {
console.log(error)
}
},
btnCancel() {
this.roleForm = {
name: '',
description: ''
}
// 移除校验
this.$refs.roleForm.resetFields()
this.showDialog = false
}
新增按钮注册事件
新增角色
提交代码
本节任务
新增角色功能
总结
我们完成了公司中角色管理的部分,但是并没有完成分配权限的部分,该部门会在权限设计和管理的部门重点提及
封装一个通用的工具栏
目标
:封装一个通用的工具栏供大家使用
通用工具栏的组件结构
在后续的业务开发中,经常会用到一个类似下图的工具栏,作为公共组件,进行一下封装
组件 src/components/PageTools/index.vue
组件统一注册
为了方便所有的页面都可以不用引用该组件,可以进行全局注册
提供注册入口 src/componets/index.js
// 该文件负责所有的公共的组件的全局注册 Vue.use
import PageTools from './PageTools'
export default {
install(Vue) {
// 注册全局的通用栏组件对象
Vue.component('PageTools', PageTools)
}
}
在入口处进行注册 src/main.js
import Component from '@/components'
Vue.use(Component) // 注册自己的插件
提交代码
本节任务
: 封装一个通用的工具栏
员工列表页面的基本布局和结构
目标
:实现员工列表页面的基本布局和结构
结构代码 src/employees/index.vue
共166条记录
导入
导出
新增员工
查看
转正
调岗
离职
角色
删除
提交代码
本节任务
:员工列表页面的基本布局和结构
员工列表数据请求和分页加载
**目标
**实现员工数据的加载和分页请求
首先,封装员工的加载请求 src/api/employees.js
/**
* 获取员工的综合列表数据
* ***/
export function getEmployeeList(params) {
return request({
url: '/sys/user',
params
})
}
然后,实现加载数据和分页的逻辑
import {
getEmployeeList } from '@/api/employees'
export default {
data() {
return {
loading: false,
list: [], // 接数据的
page: {
page: 1, // 当前页码
size: 10,
total: 0 // 总数
}
}
},
created() {
this.getEmployeeList()
},
methods: {
changePage(newPage) {
this.page.page = newPage
this.getEmployeeList()
},
async getEmployeeList() {
this.loading = true
const {
total, rows } = await getEmployeeList(this.page)
this.page.total = total
this.list = rows
this.loading = false
}
}
}
绑定表格
查看
转正
调岗
离职
角色
删除
提交代码
**本节任务
**员工列表数据请求和分页加载
员工列表中的数据进行格式化
目标
:将列表中的内容进行格式化
利用列格式化属性处理聘用形式
上小节中,列表中的聘用形式/入职时间和账户状态需要进行显示内容的处理
那么聘用形式中1代表什么含义,这实际上是我们需要的枚举数据,该数据的存放文件位于我们提供的**资源/枚举
中,可以将枚举下的文件夹放于src/api
**文件夹下
针对聘用形式,可以使用el-table-column的formatter属性进行设置
import EmployeeEnum from '@/api/constant/employees'
<!-- 格式化聘用形式 -->
<el-table-column label="聘用形式" sortable :formatter="formatEmployment" />
// 格式化聘用形式
formatEmployment(row, column, cellValue, index) {
// 要去找 1所对应的值
const obj = EmployeeEnum.hireType.find(item => item.id === cellValue)
return obj ? obj.value : '未知'
}
过滤器解决时间格式的处理
针对入职时间,我们可以采用作用域插槽进行处理
{
{
obj.row.timeOfEntry | 过滤器
}}
问题来了,过滤器从哪里呢?
在**资源/过滤器
中,我们提供了若干工具方法,我们可以将其转化成过滤器,首先将其拷贝到src
**
在**main.js
**中将工具方法转化成过滤器
import * as filters from '@/filters' // 引入工具类
// 注册全局的过滤器
Object.keys(filters).forEach(key => {
// 注册过滤器
Vue.filter(key, filters[key])
})
好了,现在可以愉快的用过滤器的方式使用工具类的方法了
{
{ row.timeOfEntry | formatDate }}
最后一项,账户状态,可以用开关组件switch进行显示
提交代码
本节任务
员工列表中的数据进行格式化
删除员工功能
**目标
**实现删除员工的功能
首先封装 删除员工的请求
/**
* 删除员工接口
* ****/
export function delEmployee(id) {
return request({
url: `/sys/user/${
id}`,
method: 'delete'
})
}
删除功能
查看
转正
调岗
离职
角色
删除
// 删除员工
async deleteEmployee(id) {
try {
await this.$confirm('您确定删除该员工吗')
await delEmployee(id)
this.getEmployeeList()
this.$message.success('删
除员工成功')
} catch (error) {
console.log(error)
}
}
提交代码
本节任务
: 删除员工功能
新增员工功能-弹层-校验-部门
目标
:实现新增员工的功能
新建员工弹层组件
当我们点击新增员工时,我们需要一个类似的弹层
类似**组织架构
**的组件,同样新建一个弹层组件 src/views/employees/components/add-employee.vue
取消
确定
引用弹出层,点击弹出
父组件中引用,弹出层
import AddDemployee from './components/add-employee'
新增员工
新增员工的表单校验
封装新增员工api src/api/employees.js
/** **
* 新增员工的接口
* **/
export function addEmployee(data) {
return request({
method: 'post',
url: '/sys/user',
data
})
}
针对员工属性,添加校验规则
import EmployeeEnum from '@/api/constant/employees'
data() {
return {
EmployeeEnum, // 在data中定义数据
// 表单数据
treeData: [], // 定义数组接收树形数据
showTree: false, // 控制树形的显示或者隐藏
loading: false, // 控制树的显示或者隐藏进度条
formData: {
username: '',
mobile: '',
formOfEmployment: '',
workNumber: '',
departmentName: '',
timeOfEntry: '',
correctionTime: ''
},
rules: {
username: [{
required: true, message: '用户姓名不能为空', trigger: 'blur' }, {
min: 1, max: 4, message: '用户姓名为1-4位'
}],
mobile: [{
required: true, message: '手机号不能为空', trigger: 'blur' }, {
pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur'
}],
formOfEmployment: [{
required: true, message: '聘用形式不能为空', trigger: 'blur' }],
workNumber: [{
required: true, message: '工号不能为空', trigger: 'blur' }],
departmentName: [{
required: true, message: '部门不能为空', trigger: 'change' }],
timeOfEntry: [{
required: true, message: '入职时间', trigger: 'blur' }]
}
}
}
绑定数据和规则校验
加载部门数据转化树形
聘用形式和选择部门的处理
员工的部门是从树形部门中选择一个部门
获取部门数据,转化树形
import {
getDepartments } from '@/api/departments'
import {
transListToTreeData } from '@/utils'
data () {
return {
// 表单数据
treeData: [], // 定义数组接收树形数据
showTree: false, // 控制树形的显示或者隐藏
loading: false, // 控制树的显示或者隐藏进度条
}
},
methods: {
async getDepartments() {
this.showTree = true
this.loading = true
const {
depts } = await getDepartments()
// depts是数组 但不是树形
this.treeData = transListToTreeData(depts, '')
this.loading = false
},
}
点击部门赋值表单数据
选择部门,赋值表单数据
点击部门时触发
selectNode(node) {
this.formData.departmentName = node.name
this.showTree = false
}
聘用形式
新增员工功能-确定-取消
调用新增接口
// 点击确定时 校验整个表单
async btnOK() {
try {
await this.$refs.addEmployee.validate()
// 调用新增接口
await addEmployee(this.formData) // 新增员工
// 告诉父组件更新数据
// this.$parent 可以直接调用到父组件的实例 实际上就是父组件this
// this.$emit
this.$parent.getEmployeeList()
this.$parent.showDialog = false
} catch (error) {
console.log(error)
}
},
btnCancel() {
// 重置原来的数据
this.formData = {
username: '',
mobile: '',
formOfEmployment: '',
workNumber: '',
departmentName: '',
timeOfEntry: '',
correctionTime: ''
}
this.$refs.addEmployee.resetFields() // 重置校验结果
this.$emit('update:showDialog', false)
}
新增员工的功能和组织架构的功能极其类似,这里不做过多阐述
提交代码
本节任务
新增员工功能和弹层
员工导入组件封装
目标
:封装一个导入excel数据的文件
首先封装一个类似的组件,首先需要注意的是,类似功能,vue-element-admin已经提供了,我们只需要改造即可 代码地址
类似功能性的组件,我们只需要会使用和封装即可
excel导入功能需要使用npm包**xlsx
,所以需要安装xlsx
**插件
$ npm i xlsx
将vue-element-admin提供的导入功能新建一个组件,位置: src/components/UploadExcel
注册全局的导入excel组件
import PageTools from './PageTools'
import UploadExcel from './UploadExcel'
export default {
install(Vue) {
Vue.component('PageTools', PageTools) // 注册工具栏组件
Vue.component('UploadExcel', UploadExcel) // 注册导入excel组件
}
}
修改样式和布局
点击上传
将文件拖到此处
提交代码
本节任务
:员工导入组件封装
员工的导入
目标
:实现员工的导入
建立公共导入的页面路由
新建一个公共的导入页面,挂载路由 src/router/index.js
{
path: '/import',
component: Layout,
hidden: true, // 隐藏在左侧菜单中
children: [{
path: '', // 二级路由path什么都不写 表示二级默认路由
component: () => import('@/views/import')
}]
},
创建import路由组件 src/views/import/index.vue
分析excel导入代码,封装接口
封装导入员工的api接口
/** *
* 封装一个导入员工的接口
*
* ***/
export function importEmployee(data) {
return request({
url: '/sys/user/batch',
method: 'post',
data
})
}
实现excel导入
获取导入的excel数据, 导入excel接口
async success({
header, results }) {
// 如果是导入员工
const userRelations = {
'入职日期': 'timeOfEntry',
'手机号': 'mobile',
'姓名': 'username',
'转正日期': 'correctionTime',
'工号': 'workNumber'
}
const arr = []
results.forEach(item => {
const userInfo = {
}
Object.keys(item).forEach(key => {
userInfo[userRelations[key]] = item[key]
})
arr.push(userInfo)
})
await importEmployee(arr) // 调用导入接口
this.$router.back()
}
为了让这个页面可以服务更多的导入功能,我们可以在页面中用参数来判断,是否是导入员工
data() {
return {
type: this.$route.query.type
}
},
当excel中有日期格式的时候,实际转化的值为一个数字,我们需要一个方法进行转化
formatDate(numb, format) {
const time = new Date((numb - 1) * 24 * 3600000 + 1)
time.setYear(time.getFullYear() - 70)
const year = time.getFullYear() + ''
const month = time.getMonth() + 1 + ''
const date = time.getDate() - 1 + ''
if (format && format.length === 1) {
return year + format + month + format + date
}
return year + (month < 10 ? '0' + month : month) + (date < 10 ? '0' + date : date)
}
需要注意,导入的手机号不能和之前的存在的手机号重复
逻辑判断
async success({
header, results }) {
if (this.type === 'user') {
const userRelations = {
'入职日期': 'timeOfEntry',
'手机号': 'mobile',
'姓名': 'username',
'转正日期': 'correctionTime',
'工号': 'workNumber'
}
const arr = []
// 遍历所有的数组
results.forEach(item => {
// 需要将每一个条数据里面的中文都换成英文
const userInfo = {
}
Object.keys(item).forEach(key => {
// key是当前的中文名 找到对应的英文名
if (userRelations[key] === 'timeOfEntry' || userRelations[key] === 'correctionTime') {
userInfo[userRelations[key]] = new Date(this.formatDate(item[key], '/')) // 只有这样, 才能入库
return
}
userInfo[userRelations[key]] = item[key]
})
// 最终userInfo变成了全是英文
arr.push(userInfo)
})
await importEmployee(arr)
this.$message.success('导入成功')
}
this.$router.back() // 回到上一页
},
formatDate(numb, format) {
const time = new Date((numb - 1) * 24 * 3600000 + 1)
time.setYear(time.getFullYear() - 70)
const year = time.getFullYear() + ''
const month = time.getMonth() + 1 + ''
const date = time.getDate() - 1 + ''
if (format && format.length === 1) {
return year + format + month + format + date
}
return year + (month < 10 ? '0' + month : month) + (date < 10 ? '0' + date : date)
}
员工页面跳转
导入
目标
: 实现员工的导入
员工导出excel功能
目标: 实现将员工数据导出功能
日常业务中,我们经常遇到excel导出功能, 怎么使用呢
Excel 的导入导出都是依赖于js-xlsx来实现的。
在 js-xlsx
的基础上又封装了Export2Excel.js来方便导出数据。
安装excel所需依赖和按需加载
由于 Export2Excel
不仅依赖js-xlsx
还依赖file-saver
和script-loader
。
所以你先需要安装如下命令:
npm install xlsx file-saver -S
npm install script-loader -S -D
由于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导出基本的结构
我们最重要的一件事,就是把表头和数据进行相应的对应
因为数据中的key是英文,想要导出的表头是中文的话,需要将中文和英文做对应
const headers = {
'手机号': 'mobile',
'姓名': 'username',
'入职日期': 'timeOfEntry',
'聘用形式': 'formOfEmployment',
'转正日期': 'correctionTime',
'工号': 'workNumber',
'部门': 'departmentName'
}
然后,完成导出代码
// 导出excel数据
exportData() {
// 做操作
// 表头对应关系
const headers = {
'姓名': 'username',
'手机号': 'mobile',
'入职日期': 'timeOfEntry',
'聘用形式': 'formOfEmployment',
'转正日期': 'correctionTime',
'工号': 'workNumber',
'部门': 'departmentName'
}
// 懒加载
import('@/vendor/Export2Excel').then(async excel => {
const {
rows } = await getEmployeeList({
page: 1, size: this.page.total })
const data = this.formatJson(headers, rows)
excel.export_json_to_excel({
header: Object.keys(headers),
data,
filename: '员工信息表',
autoWidth: true,
bookType: 'xlsx'
})
// 获取所有的数据
// excel.export_json_to_excel({
// header: ['姓名', '薪资'],
// data: [['张三', 12000], ['李四', 5000]],
// filename: '员工薪资表',
// autoWidth: true,
// bookType: 'csv'
// })
})
},
// 该方法负责将数组转化成二维数组
formatJson(headers, rows) {
// 首先遍历数组
// [{ username: '张三'},{},{}] => [[’张三'],[],[]]
return rows.map(item => {
return Object.keys(headers).map(key => {
if (headers[key] === 'timeOfEntry' || headers[key] === 'correctionTime') {
return formatDate(item[headers[key]]) // 返回格式化之前的时间
} else if (headers[key] === 'formOfEmployment') {
var en = EmployeeEnum.hireType.find(obj => obj.id === item[headers[key]])
return en ? en.value : '未知'
}
return item[headers[key]]
}) // => ["张三", "13811","2018","1", "2018", "10002"]
})
// return data
// return rows.map(item => {
// // item是对象 => 转化成只有值的数组 => 数组值的顺序依赖headers {username: '张三' }
// // Object.keys(headers) => ["姓名", "手机号",...]
// return Object.keys(headers).map(key => {
// return item[headers[key]]
// }) // / 得到 ['张三',’129‘,’dd‘,'dd']
// })
}
导出时间格式的处理
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]]))
// 需要处理时间格式问题
}
扩展
复杂表头的导出
当需要导出复杂表头的时候,vue-element-admin同样支持该类操作
vue-element-admin 提供的导出方法中有 multiHeader和merges 的参数
参数
说明
类型
可选值
默认值
multiHeader
复杂表头的部分
Array
/
[[]]
merges
需要合并的部分
Array
/
[]
multiHeader里面是一个二维数组,里面的一个元素是一行表头,假设你想得到一个如图的结构
mutiHeader应该这样定义
const multiHeader = [['姓名', '主要信息', '', '', '', '', '部门']]
multiHeader中的一行表头中的字段的个数需要和真正的列数相等,假设想要跨列,多余的空间需要定义成空串
它主要对应的是标准的表头
const header = ['姓名', '手机号', '入职日期', '聘用形式', '转正日期', '工号', '部门']
如果,我们要实现其合并的效果, 需要设定merges选项
const merges = ['A1:A2', 'B1:F1', 'G1:G2']
merges的顺序是没关系的,只要配置这两个属性,就可以导出复杂表头的excel了
exportData() {
const headers = {
'姓名': 'username',
'手机号': 'mobile',
'入职日期': 'timeOfEntry',
'聘用形式': 'formOfEmployment',
'转正日期': 'correctionTime',
'工号': 'workNumber',
'部门': 'departmentName'
}
// 导出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]]))
// 需要处理时间格式问题
}
提交代码
**本节任务
**实现将员工数据导出功能
员工详情页创建和布局
目标
:创建员工详情的主要布局页面和基本布局
详情页的基本布局和路由
建立详情页路由
{
path: 'detail/:id', // query传参 动态路由传参
component: () => import('@/views/employees/detail'),
hidden: true, // 不在左侧菜单显示
meta: {
title: '员工详情' // 标记当前路由规则的中文名称 后续在做左侧菜单时 使用
}
}
建立基本架构
更新
列表跳转到详情
查看
读取和保存用户信息的接口
加载个人基本信息 > 该接口已经在之前提供过了 src/api/user.js
/** *
* 获取某个用户的基本信息
*
* ***/
export function getUserDetailById(id) {
return request({
url: `/sys/user/${
id}`
})
}
保存个人基本信息 src/api/employees.js
/** *
*
* 保存员工的基本信息
* **/
export function saveUserDetailById(data) {
return request({
url: `/sys/user/${
data.id}`,
method: 'put',
data
})
}
实现用户名和密码的修改
注意
:这里有个缺陷,接口中读取的是后端的密文,我们并不能解密,所以当我们没有任何修改就保存时,会校验失败,因为密文超过了规定的12位长度,为了真的修改密码,我们设定了一个临时的字段 password2,用它来存储我们的修改值,最后保存的时候,把password2传给password
用户名和密码的修改 src/views/employees/detail.vue
import {
getUserDetailById } from '@/api/user'
import {
saveUserDetailById } from '@/api/employees'
export default {
data() {
return {
userId: this.$route.params.id, // 这样可以后面直接通过 this.userId进行获取数据
userInfo: {
// 专门存放基本信息
username: '',
password2: ''
},
rules: {
username: [{
required: true, message: '姓名不能为空', trigger: 'blur' }],
password2: [{
required: true, message: '密码不能为空', trigger: 'blur' },
{
min: 6, max: 9, message: '密码长度6-9位', trigger: 'blur' }]
}
}
},
created() {
this.getUserDetailById()
},
methods: {
async getUserDetailById() {
this.userInfo = await getUserDetailById(this.userId)
},
async saveUser() {
try {
// 校验
await this.$refs.userForm.validate()
await saveUserDetailById({
...this.userInfo, password: this.userInfo.password2 }) // 将新密码的值替换原密码的值
this.$message.success('保存成功')
} catch (error) {
console.log(error)
}
}
}
}
绑定表单数据
更新
提交代码
个人组件和岗位组件封装
封装个人详情组件
我们将员工个人信息分为三部分,账户,个人, 岗位,这个小节我们对个人组件和岗位组件进行封装
封装个人组件 src/views/employees/components/user-info.vue
本章节个人数据过于**繁杂,庞大
**,同学们在开发期间,拷贝代码即可,我们只写关键部位的代码
定义user-info的数据
import EmployeeEnum from '@/api/constant/employees'
export default {
data() {
return {
userId: this.$route.params.id,
EmployeeEnum, // 员工枚举数据
userInfo: {
},
formData: {
userId: '',
username: '', // 用户名
sex: '', // 性别
mobile: '', // 手机
companyId: '', // 公司id
departmentName: '', // 部门名称
// onTheJobStatus: '', // 在职状态 no
dateOfBirth: '', // 出生日期
timeOfEntry: '', // 入职时间
theHighestDegreeOfEducation: '', // 最高学历
nationalArea: '', // 国家
passportNo: '', // 护照号
idNumber: '', // 身份证号
idCardPhotoPositive: '', // 身份证照正
idCardPhotoBack: '', // 身份证照正
nativePlace: '', // 籍贯
nation: '', // 民族
englishName: '', // 英文名字
maritalStatus: '', // 婚姻状况
staffPhoto: '', // 员工照片
birthday: '', // 生日
zodiac: '', // 属相
age: '', // 年龄
constellation: '', // 星座
bloodType: '', // 血型
domicile: '', // 户籍所在地
politicalOutlook: '', // 政治面貌
timeToJoinTheParty: '', // 入党时间
archivingOrganization: '', // 存档机构
stateOfChildren: '', // 子女状态
doChildrenHaveCommercialInsurance: '1', // 保险状态
isThereAnyViolationOfLawOrDiscipline: '', // 违法违纪状态
areThereAnyMajorMedicalHistories: '', // 重大病史
qq: '', // QQ
wechat: '', // 微信
residenceCardCity: '', // 居住证城市
dateOfResidencePermit: '', // 居住证办理日期
residencePermitDeadline: '', // 居住证截止日期
placeOfResidence: '', // 现居住地
postalAddress: '', // 通讯地址
contactTheMobilePhone: '', // 联系手机
personalMailbox: '', // 个人邮箱
emergencyContact: '', // 紧急联系人
emergencyContactNumber: '', // 紧急联系电话
socialSecurityComputerNumber: '', // 社保电脑号
providentFundAccount: '', // 公积金账号
bankCardNumber: '', // 银行卡号
openingBank: '', // 开户行
educationalType: '', // 学历类型
graduateSchool: '', // 毕业学校
enrolmentTime: '', // 入学时间
graduationTime: '', // 毕业时间
major: '', // 专业
graduationCertificate: '', // 毕业证书
certificateOfAcademicDegree: '', // 学位证书
homeCompany: '', // 上家公司
title: '', // 职称
resume: '', // 简历
isThereAnyCompetitionRestriction: '', // 有无竞业限制
proofOfDepartureOfFormerCompany: '', // 前公司离职证明
remarks: '' // 备注
}
}
}
}
在detail.vue组件中,注册并使用
在以上代码中,我们使用了动态组件component,它通过 **is
属性来绑定需要显示在该位置的组件,is属性可以直接为注册组件
**的组件名称即可
封装岗位组件
同理,封装岗位组件
封装岗位组件 src/views/employee/components/job-info.vue
基础信息
合同信息
招聘信息
保存更新
返回
定义岗位数据
import EmployeeEnum from '@/api/constant/employees'
export default {
data() {
return {
userId: this.$route.params.id,
depts: [],
EmployeeEnum,
formData: {
adjustmentAgedays: '', // 调整司龄天
adjustmentOfLengthOfService: '', // 调整工龄天
closingTimeOfCurrentContract: '', // 现合同结束时间
companyId: '', // 公司ID
contractDocuments: '', // 合同文件
contractPeriod: '', // 合同期限
correctionEvaluation: '', // 转正评价
currentContractStartTime: '', // 现合同开始时间
firstContractTerminationTime: '', // 首次合同结束时间
hrbp: '', // HRBP
initialContractStartTime: '', // 首次合同开始时间
otherRecruitmentChannels: '', // 其他招聘渠道
post: '', // 岗位
rank: null, // 职级
recommenderBusinessPeople: '', // 推荐企业人
recruitmentChannels: '', // 招聘渠道
renewalNumber: '', // 续签次数
reportId: '', // 汇报对象
reportName: null, // 汇报对象
socialRecruitment: '', // 社招校招
stateOfCorrection: '', // 转正状态
taxableCity: '', // 纳税城市
userId: '', // 员工ID
workMailbox: '', // 工作邮箱
workingCity: '', // 工作城市
workingTimeForTheFirstTime: '' // 首次参加工作时间
}
}
}
}
在detail.vue组件中,注册并使用
本节任务
:完成个人组件和岗位组件封装
员工个人信息和岗位信息-读取-保存
目标
:实现个人信息的岗位信息的读取和校验,保存
读取个人保存个人信息
这个环节里面大部分都是繁杂的属性和重复的过程,所以该环节直接将过程代码拷贝到项目中即可
封装 读取个人信息 保存个人信息 读取岗位信息 保存岗位信息
/** *
* 读取用户详情的基础信息
* **/
export function getPersonalDetail(id) {
return request({
url: `/employees/${
id}/personalInfo`
})
}
/** *
* 更新用户详情的基础信息
* **/
export function updatePersonal(data) {
return request({
url: `/employees/${
data.userId}/personalInfo`,
method: 'put',
data
})
}
/** **
* 获取用户的岗位信息
*
* ****/
export function getJobDetail(id) {
return request({
url: `/employees/${
id}/jobs`
})
}
/**
* 保存岗位信息
* ****/
export function updateJob(data) {
return request({
url: `/employees/${
data.userId}/jobs`,
method: 'put',
data
})
}
读取,保存个人信息 user-info
需要注意:这里的保存实际上分成了两个接口,这是接口的设计,我们只能遵守
import {
getPersonalDetail, updatePersonal, saveUserDetailById } from '@/api/employees'
import {
getUserDetailById } from '@/api/user'
created() {
this.getPersonalDetail()
this.getUserDetailById()
},
methods: {
async getPersonalDetail() {
this.formData = await getPersonalDetail(this.userId) // 获取员工数据
},
async savePersonal() {
await updatePersonal({
...this.formData, id: this.userId })
this.$message.success('保存成功')
},
async saveUser() {
// 调用父组件
await saveUserDetailById(this.userInfo)
this.$message.success('保存成功')
},
async getUserDetailById() {
this.userInfo = await getUserDetailById(this.userId)
}
}
读取保存岗位信息
读取,保存岗位信息 job-info
import {
getEmployeeSimple, updateJob, getJobDetail } from '@/api/employees'
created() {
this.getJobDetail()
this.getEmployeeSimple()
},
methods: {
async getJobDetail() {
this.formData = await getJobDetail(this.userId)
},
// 获取员工列表
async getEmployeeSimple() {
this.depts = await getEmployeeSimple()
},
// 保存岗位信息
async saveJob() {
await updateJob(this.formData)
this.$message.success('保存岗位信息成功')
}
}
提交代码
本节任务
实现个人信息的岗位信息的读取和校验,保存
配置腾讯云Cos
目标
: 配置一个腾讯云cos
由于上课的开发的特殊性,我们不希望把所有的图片都上传到我们自己的官方服务器上,这里我们可以采用一个腾讯云的图片方案
上边图的意思就是说,我们找一个可以免费上传图片的服务器,帮我们**代管图片
,我们在自己的数据库里只保存一个地址就行, 这其实也是很多项目的处理方案,会有一个公共的文件服务器
**
第一步,我们必须先拥有一个腾迅云的开发者账号(小心腾讯云的广告电话)
请按照腾讯云的注册方式,注册自己的账号
第二步,实名认证
选择个人账户
填写个人身份信息
下一步,扫描二维码授权
手机端授权
点击领取免费产品
选择对象存储COS
我们免费拥有**6个月的50G流量
**的对象存储空间使用权限,足够我们上传用户头像的使用了
点击0元试用,开通服务
到这一步,账号的部分就操作完毕,接下来,我们需要来创建一个存储图片的存储桶
登录 对象存储控制台 ,创建存储桶。设置存储桶的权限为 公有读,私有写
设置cors规则
AllowHeader 需配成*
,如下图所示。
因为我们本身没有域名,所以这里设置成***
**,仅限于测试,正式环境的话,这里需要配置真实的域名地址
到这里,我们的腾讯云存储桶就设置好了。
封装上传图片组件-上传组件需求分析
目标
梳理整个的上传过程
初始化cos对象参数
名称
描述
SecretId
开发者拥有的项目身份识别 ID,用以身份认证,可在 API 密钥管理 页面获取
SecretKey
开发者拥有的项目身份密钥,可在 API 密钥管理 页面获取
注意,上述的参数我们在本次开发过程中,直接将参数放置在前端代码中存储,但是腾讯云本身是不建议这么做的,因为**敏感信息
**放在前端很容易被捕获,由于我们本次是测试研发,所以这个过程可以忽略
正确的做法应该是,通过网站调用接口换取敏感信息
相关文档
实例化 上传sdk
var cos = new COS({
SecretId: 'COS_SECRETID', // 身份识别 ID
SecretKey: 'COS_SECRETKEY', // 身份密钥
});
到目前为止,我们上传图片准备的内容就已经OK,接下来,我们在**src/componets
** 新建一个**ImageUpload
** 组件
该组件需要满足什么要求呢?
- 可以显示传入的图片地址
- 可以删除传入的图片地址
- 可以上传图片到云服务器
- 上传到腾讯云之后,可以返回图片地址,显示
- 上传成功之后,可以回调成功函数
这个上传组件简单吗?
no ! ! !
看似需求很明确,但是它真正的实现很复杂,我们通过一个图来看一下
从上图中,我们可以看到,实际上是有两种场景的,本地场景和已经上传的场景
下个章节,针对这个场景我们进行开发
封装上传组件-代码实现
**目标
**实现上传组件的代码部分
JavaScript SDK 需浏览器支持基本的 HTML5 特性(支持 IE10 以上浏览器),以便支持 ajax 上传文件和计算文件 MD5 值。
新建文件上传组件
安装JavaScript SDK
$ npm i cos-js-sdk-v5 --save
新建上传图片组件 src/components/ImageUpload/index.vue
上传组件,我们可以沿用element的el-upload组件,并且采用照片墙的模式 list-type="picture-card"
放置el-upload组件
全局注册组件
import PageTools from './PageTools'
import UploadExcel from './UploadExcel'
import ImageUpload from './ImageUpload'
export default {
install(Vue) {
Vue.component('PageTools', PageTools) // 注册工具栏组件
Vue.component('UploadExcel', UploadExcel) // 注册导入excel组件
Vue.component('ImageUpload', ImageUpload) // 注册导入上传组件
}
}
点击图片进行预览
限定上传的图片数量和action
action为什么给#, 因为前面我们讲过了,我们要上传到腾讯云,需要自定义的上传方式,action给个#防止报错
预览
data() {
return {
fileList: [], // 图片地址设置为数组
showDialog: false, // 控制显示弹层
https://gitee.com//owahahah/hrsass/raw/master/imgUrl: ''
}
},
preview(file) {
// 这里应该弹出一个层 层里是点击的图片地址
this.https://gitee.com//owahahah/hrsass/raw/master/imgUrl = file.url
this.showDialog = true
},
预览弹层
根据上传数量控制上传按钮
控制上传显示
computed: {
// 设定一个计算属性 判断是否已经上传完了一张
fileComputed() {
return this.fileList.length === 1
}
},
{
disabled: fileComputed }"
>
删除图片和添加图片
删除文件
handleRemove(file) {
// file是点击删除的文件
// 将原来的文件给排除掉了 剩下的就是最新的数组了
this.fileList = this.fileList.filter(item => item.uid !== file.uid)
},
添加文件
// 修改文件时触发
// 此时可以用fileList 因为该方法会进来很多遍 不能每次都去push
// fileList因为fileList参数是当前传进来的最新参数 我们只需要将其转化成数组即可 需要转化成一个新的数组
// [] => [...fileList] [] => fileList.map()
// 上传成功之后 还会进来 需要实现上传代码的逻辑 这里才会成功
changeFile(file, fileList) {
this.fileList = fileList.map(item => item)
}
上传之前检查
控制上传图片的类型和上传大小, 如果不满足条件 返回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
}
上传动作调用上传腾讯云
上传动作为el-upload的http-request属性
:http-request="upload"
// 自定义上传动作 有个参数 有个file对象,是我们需要上传到腾讯云服务器的内容
upload(params) {
console.log(params.file)
}
我们需要在该方法中,调用腾讯云的上传方法
腾讯云文档地址
身份ID和密钥可以通过腾讯云平台获取
登录 访问管理控制台 ,获取您的项目 SecretId 和 SecretKey。
实现代码
// 进行上传操作
upload(params) {
// console.log(params.file)
if (params.file) {
// 执行上传操作
cos.putObject({
Bucket: 'shuiruohanyu-106-1302806742', // 存储桶
Region: 'ap-beijing', // 地域
Key: params.file.name, // 文件名
Body: params.file, // 要上传的文件对象
StorageClass: 'STANDARD' // 上传的模式类型 直接默认 标准模式即可
// 上传到腾讯云 =》 哪个存储桶 哪个地域的存储桶 文件 格式 名称 回调
}, function(err, data) {
// data返回数据之后 应该如何处理
console.log(err || data)
})
}
}
上传成功之后处理返回数据
如何处理返回成功的返回数据
确定要上传记录id
beforeUpload(file) {
// 先检查文件类型
const types = ['image/jpeg', 'image/gif', 'image/bmp', 'image/png']
if (!types.some(item => item === file.type)) {
// 如果不存在
this.$message.error('上传图片只能是 JPG、GIF、BMP、PNG 格式!')
return false // 上传终止
}
// 检查文件大小 5M 1M = 1024KB 1KB = 1024B
const maxSize = 5 * 1024 * 1024
if (file.size > maxSize) {
// 超过了限制的文件大小
this.$message.error('上传的图片大小不能大于5M')
return false
}
// 已经确定当前上传的就是当前的这个file了
this.currentFileUid = file.uid
return true // 最后一定要return true
},
处理返回数据
// 进行上传操作
upload(params) {
// console.log(params.file)
if (params.file) {
// 执行上传操作
cos.putObject({
Bucket: 'shuiruohanyu-106-1302806742', // 存储桶
Region: 'ap-beijing', // 地域
Key: params.file.name, // 文件名
Body: params.file, // 要上传的文件对象
StorageClass: 'STANDARD' // 上传的模式类型 直接默认 标准模式即可
// 上传到腾讯云 =》 哪个存储桶 哪个地域的存储桶 文件 格式 名称 回调
}, (err, data) => {
// data返回数据之后 应该如何处理
console.log(err || data)
// data中有一个statusCode === 200 的时候说明上传成功
if (!err && data.statusCode === 200) {
// 此时说明文件上传成功 要获取成功的返回地址
// fileList才能显示到上传组件上 此时我们要将fileList中的数据的url地址变成 现在上传成功的地址
// 目前虽然是一张图片 但是请注意 我们的fileList是一个数组
// 需要知道当前上传成功的是哪一张图片
this.fileList = this.fileList.map(item => {
// 去找谁的uid等于刚刚记录下来的id
if (item.uid === this.currentFileUid) {
// 将成功的地址赋值给原来的url属性
return {
url: 'http://' + data.Location, upload: true }
// upload 为true 表示这张图片已经上传完毕 这个属性要为我们后期应用的时候做标记
// 保存 => 图片有大有小 => 上传速度有快又慢 =>要根据有没有upload这个标记来决定是否去保存
}
return item
})
// 将上传成功的地址 回写到了fileList中 fileList变化 =》 upload组件 就会根据fileList的变化而去渲染视图
}
})
}
}
我们在fileList中设置了属性为upload为true的属性,表示该图片已经上传成功了,如果fileList还有upload不为true的数据,那就表示该图片还没有上传完毕
上传的进度条显示
为了再上传图片过程中显示进度条,我们可以使用element-ui的进度条显示当前的上传进度
放置进度条
通过腾讯云sdk监听上传进度
cos.putObject({
// 配置
Bucket: 'laogao-1302806742', // 存储桶名称
Region: 'ap-guangzhou', // 存储桶地域
Key: params.file.name, // 文件名作为key
StorageClass: 'STANDARD', // 此类写死
Body: params.file, // 将本地的文件赋值给腾讯云配置
// 进度条
onProgress: (params) => {
this.percent = params.percent * 100
}
}
完整代码
上传动作中,用到了上个小节中,我们注册的腾讯云cos的**存储桶名称
和地域名称
**
通过上面的代码,我们会发现,我们把上传之后的图片信息都给了**fileList数据
,那么在应用时,就可以直接获取该实例的fileList数据即可
**
提交代码
本节任务
完成上传组件的封装
在员工详情中应用上传组件
目标
:应用封装好的上传组件
将员工的头像和证件照赋值给上传组件
在**user-info.vue
**中放置上传组件
员工头像
读取时赋值头像
// 读取上半部分的内容
async getUserDetailById() {
this.userInfo = await getUserDetailById(this.userId)
if (this.userInfo.staffPhoto) {
// 这里我们赋值,同时需要给赋值的地址一个标记 upload: true
this.$refs.staffPhoto.fileList = [{
url: this.userInfo.staffPhoto, upload: true }]
}
},
员工证件照
读取时赋值照片
// 读取下半部分内容
async getPersonalDetail() {
this.formData = await getPersonalDetail(this.userId)
if (this.formData.staffPhoto) {
this.$refs.myStaffPhoto.fileList = [{
url: this.formData.staffPhoto, upload: true }]
}
},
保存时处理头像和证件照的保存
当点击保存更新时,获取图片的内容
async saveUser() {
// 去读取 员工上传的头像
const fileList = this.$refs.staffPhoto.fileList // 读取上传组件的数据
if (fileList.some(item => !item.upload)) {
// 如果此时去找 upload为false的图片 找到了说明 有图片还没有上传完成
this.$message.warning('您当前还有图片没有上传完成!')
return
}
// 通过合并 得到一个新对象
await saveUserDetailById({
...this.userInfo, staffPhoto: fileList && fileList.length ? fileList[0].url : '' })
this.$message.success('保存基本信息成功')
},
上面代码中,upload如果为true,表示该图片已经完成上传,以此来判断图片是否已经上传完成
保存时读取头像
async savePersonal() {
const fileList = this.$refs.myStaffPhoto.fileList
if (fileList.some(item => !item.upload)) {
// 如果此时去找 upload为false的图片 找到了说明 有图片还没有上传完成
this.$message.warning('您当前还有图片没有上传完成!')
return
}
await updatePersonal({
...this.formData, staffPhoto: fileList && fileList.length ? fileList[0].url : '' })
this.$message.success('保存基础信息成功')
}
提交代码
本节任务
: 在员工详情中应用上传组件
员工列表显示图片
目标
:在员工列表中心显示图片
员工的头像可以在列表项中添加一列来进行显示
我们尝试用之前的指令来处理图片的异常问题,但是发现只有两三张图片能显示
这是因为有的员工的头像的地址为空,给https://gitee.com//owahahah/hrsass/raw/master/img赋值空的src不能触发错误事件,针对这一特点,我们需要对指令进行升级
插入节点的钩子里面判断空, 然后在组件更新之后的钩子中同样判断空
export const imageerror = {
inserted(dom, options) {
// 图片异常的逻辑
// 监听https://gitee.com//owahahah/hrsass/raw/master/img标签的错误事件 因为图片加载失败 会触发 onerror事件
dom.src = dom.src || options.value
dom.onerror = function() {
// 图片失败 赋值一个默认的图片
dom.src = options.value
}
},
componentUpdated(dom, options) {
dom.src = dom.src || options.value
}
}
这样我们可以看到每个用户的头像了,如果没有头像则显示默认图片
任务
:员工列表显示图片
图片地址生成二维码
目标
将图片地址生成二维码显示
我们想完成这样一个功能,当我们拥有头像地址时,将头像地址生成一个二维码,用手机扫码来访问
首先,需要安装生成二维码的插件
$ npm i qrcode
qrcode的用法是
QrCode.toCanvas(dom, info)
dom为一个canvas的dom对象, info为转化二维码的信息
我们尝试将canvas标签放到dialog的弹层中
在点击员工的图片时,显示弹层,并将图片地址转化成二维码
showQrCode(url) {
// url存在的情况下 才弹出层
if (url) {
this.showCodeDialog = true // 数据更新了 但是我的弹层会立刻出现吗 ?页面的渲染是异步的!!!!
// 有一个方法可以在上一次数据更新完毕,页面渲染完毕之后
this.$nextTick(() => {
// 此时可以确认已经有ref对象了
QrCode.toCanvas(this.$refs.myCanvas, url) // 将地址转化成二维码
// 如果转化的二维码后面信息 是一个地址的话 就会跳转到该地址 如果不是地址就会显示内容
})
} else {
this.$message.warning('该用户还未上传头像')
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QgKPjy1S-1610539647209)(https://gitee.com//owahahah/hrsass/raw/master/img/image-20201028180651472.png)]
打印员工信息
目标
完成个人信息和工作信息的打印功能
新建打印页面及路由
创建页面组件
首页
员工管理
打印
员工信息表
基本信息
姓名
{
{ formData.username }}
性别
{
{ formData.sex }}
手机
{
{ formData.mobile }}
出生日期
{
{ formData.dateOfBirth | formatDate }}
最高学历
{
{ formData.theHighestDegreeOfEducation }}
是否可编辑
{
{ formData.isItEditable }}
是否隐藏号码
{
{ formData.doYouHideNumbers }}
国家地区
{
{ formData.nationalArea }}
护照号
{
{ formData.passportNo }}
身份证号
{
{ formData.idNumber }}
身份证照片
{
{ formData.iDCardPhoto }}
籍贯
{
{ formData.nativePlace }}
民族
{
{ formData.nation }}
英文名
{
{ formData.englishName }}
婚姻状况
{
{ formData.maritalStatus }}
员工照片
{
{ formData.staffPhoto }}
生日
{
{ formData.birthday }}
属相
{
{ formData.zodiac }}
年龄
{
{ formData.age }}
星座
{
{ formData.constellation }}
血型
{
{ formData.bloodType }}
户籍所在地
{
{ formData.domicile }}
政治面貌
{
{ formData.politicalOutlook }}
入党时间
{
{ formData.timeToJoinTheParty }}
存档机构
{
{ formData.archivingOrganization }}
子女状态
{
{ formData.stateOfChildren }}
子女有无商业保险
{
{ formData.doChildrenHaveCommercialInsurance }}
有无违法违纪行为
{
{ formData.isThereAnyViolationOfLawOrDiscipline }}
有无重大病史
{
{ formData.areThereAnyMajorMedicalHistories }}
通讯信息
QQ
{
{ formData.qQ }}
微信
{
{ formData.weChat }}
居住证城市
{
{ formData.residenceCardCity }}
居住证办理日期
{
{ formData.dateOfResidencePermit }}
居住证截止日期
{
{ formData.residencePermitDeadline }}
现居住地
{
{ formData.placeOfResidence }}
通讯地址
{
{ formData.postalAddress }}
联系手机
{
{ formData.contactTheMobilePhone }}
个人邮箱
{
{ formData.personalMailbox }}
紧急联系人
{
{ formData.emergencyContact }}
紧急联系电话
{
{ formData.emergencyContactNumber }}
账号信息
社保电脑号
{
{ formData.socialSecurityComputerNumber }}
公积金账号
{
{ formData.providentFundAccount }}
银行卡号
{
{ formData.bankCardNumber }}
开户行
{
{ formData.openingBank }}
教育信息
学历类型
{
{ formData.educationalType }}
毕业学校
{
{ formData.graduateSchool }}
入学时间
{
{ formData.enrolmentTime }}
毕业时间
{
{ formData.graduationTime }}
专业
{
{ formData.major }}
毕业证书
{
{ formData.graduationCertificate }}
学位证书
{
{ formData.certificateOfAcademicDegree }}
从业信息
上家公司
{
{ formData.homeCompany }}
职称
{
{ formData.title }}
简历
{
{ formData.resume }}
有无竞业限制
{
{ formData.isThereAnyCompetitionRestriction }}
前公司离职证明
{
{ formData.proofOfDepartureOfFormerCompany }}
备注
{
{ formData.remarks }}
签字:___________日期:___________
岗位信息表
基本信息
姓名
{
{ formData.username }}
入职日期
{
{ formData.dateOfEntry }}
部门
{
{ formData.departmentName }}
岗位
{
{ formData.post }}
工作邮箱
{
{ formData.workMailbox }}
工号
{
{ formData.workNumber }}
转正日期
{
{ formData.dateOfCorrection }}
转正状态
{
{ formData.stateOfCorrection }}
职级
{
{ formData.rank }}
汇报对象
{
{ formData.reportName }}
HRBP
{
{ formData.hRBP }}
聘用形式
{
{ formData.formOfEmployment }}
管理形式
{
{ formData.formOfManagement }}
调整司龄
{
{ formData.adjustmentAgedays }}
司龄
{
{ formData.ageOfDivision }}
首次参加工作时间
{
{ formData.workingTimeForTheFirstTime }}
调整工龄天
{
{ formData.adjustmentOfLengthOfService }}
工龄
{
{ formData.workingYears }}
纳税城市
{
{ formData.taxableCity }}
转正评价
{
{ formData.correctionEvaluation }}
合同信息
首次合同开始时间
{
{ formData.initialContractStartTime }}
首次合同结束时间
{
{ formData.firstContractTerminationTime }}
现合同开始时间
{
{ formData.currentContractStartTime }}
现合同结束时间
{
{ formData.closingTimeOfCurrentContract }}
合同期限
{
{ formData.contractPeriod }}
合同文件
{
{ formData.contractDocuments }}
续签次数
{
{ formData.renewalNumber }}
招聘信息
其他招聘渠道
{
{ formData.otherRecruitmentChannels }}
招聘渠道
{
{ formData.recruitmentChannels }}
社招校招
{
{ formData.socialRecruitment }}
推荐企业人
{
{ formData.recommenderBusinessPeople }}
签字:___________日期:___________
该页面内容实际上就是读取个人和详情的接口数据,根据传入的type类型决定显示个人还是岗位
type为**personal
时显示个人,为job
**时显示岗位
新建打印页面路由
{
path: 'print/:id', // 二级默认路由
component: () => import('@/views/employees/print'), // 按需加载
hidden: true,
meta: {
title: '打印', // 标记当前路由规则的中文名称 后续在做左侧菜单时 使用
icon: 'people'
}
}
完成详情到打印的跳转
个人
岗位
利用vue-print-nb进行打印
首先,打印功能我们借助一个比较流行的插件
$ npm i vue-print-nb
它的用法是
首先注册该插件
import Print from 'vue-print-nb'
Vue.use(Print);
使用v-print指令的方式进行打印
打印
printObj: {
id: 'myPrint'
}
最终,我们看到的效果
提交代码
**本节任务
**打印员工信息
权限设计-RBAC的权限设计思想
首先,我们先了解下什么是传统的权限设计
从上面的图中,我们发现,传统的权限设计是对每个人进行单独的权限设置,但这种方式已经不适合目前企业的高效管控权限的发展需求,因为每个人都要单独去设置权限
基于此,RBAC的权限模型就应运而生了,RBAC(Role-Based Access control) ,也就是基于角色的权限分配解决方案,相对于传统方案,RBAC提供了中间层Role(角色),其权限模式如下
RBAC实现了用户和权限点的分离,想对某个用户设置权限,只需要对该用户设置相应的角色即可,而该角色就拥有了对应的权限,这样一来,权限的分配和设计就做到了极简,高效,当想对用户收回权限时,只需要收回角色即可,接下来,我们就在该项目中实施这一设想
给分配员工角色
**目标
**在员工管理页面,分配角色
新建分配角色窗体
在上一节章节中,员工管理的角色功能,我们并没有实现,此章节我们实现给员工分配角色
从上图中,可以看出,用户和角色是**1对多
**的关系,即一个用户可以拥有多个角色,比如公司的董事长可以拥有总经理和系统管理员一样的角色
首先,新建分配角色窗体 assign-role.vue
确定
取消
获取角色列表和当前用户角色
获取所有角色列表
{
{
item.name
}}
获取角色列表
import {
getRoleList } from '@/api/setting'
export default {
props: {
showRoleDialog: {
type: Boolean,
default: false
},
userId: {
type: String,
default: null
}
},
data() {
return {
list: [], // 角色列表
roleIds: []
}
},
created() {
this.getRoleList()
},
methods: {
// 获取所有角色
async getRoleList() {
const {
rows } = await getRoleList()
this.list = rows
}
}
}
获取用户的当前角色
import {
getUserDetailById } from '@/api/user'
async getUserDetailById(id) {
const {
roleIds } = await getUserDetailById(id)
this.roleIds = roleIds // 赋值本用户的角色
}
点击角色弹出层
// 编辑角色
async editRole(id) {
this.userId = id // props传值 是异步的
await this.$refs.assignRole.getUserDetailById(id) // 父组件调用子组件方法
this.showRoleDialog = true
},
给员工分配角色
分配角色接口 api/employees.js
/** *
* 给用户分配角色
* ***/
export function assignRoles(data) {
return request({
url: '/sys/user/assignRoles',
data,
method: 'put'
})
}
确定保存 assign-role
async btnOK() {
await assignRoles({
id: this.userId, roleIds: this.roleIds })
// 关闭窗体
this.$emit('update:showRoleDialog', false)
},
**取消或者关闭 ** assign-role
btnCancel() {
this.roleIds = [] // 清空原来的数组
this.$emit('update:showRoleDialog', false)
}
提交代码
本节任务
分配员工权限
权限点管理页面开发
目标
: 完成权限点页面的开发和管理
新建权限点管理页面
人已经有了角色, 那么权限是什么
在企业服务中,权限一般分割为 页面访问权限,按钮操作权限,API访问权限
API权限多见于在后端进行拦截,所以我们这一版本只做**页面访问
和按钮操作授权
/**
由此,我们可以根据业务需求设计权限管理页面
完成权限页面结构 src/views/permission/index.vue
添加权限
添加
编辑
删除
封装权限管理的增删改查请求 src/api/permisson.js
// 获取权限
export function getPermissionList(params) {
return request({
url: '/sys/permission',
params
})
}
// 新增权限
export function addPermission(data) {
return request({
url: '/sys/permission',
method: 'post',
data
})
}
// 更新权限
export function updatePermission(data) {
return request({
url: `/sys/permission/${
data.id}`,
method: 'put',
data
})
}
// 删除权限
export function delPermission(id) {
return request({
url: `/sys/permission/${
id}`,
method: 'delete'
})
}
// 获取权限详情
export function getPermissionDetail(id) {
return request({
url: `/sys/permission/${
id}`
})
}
获取权限数据并转化树形
这里,我们通过树形操作方法,将列表转化成层级数据
绑定表格数据
添加
编辑
删除
需要注意的是, 如果需要树表, 需要给el-table配置row-key属性 id
当type为1时为访问权限,type为2时为功能权限
和前面内容一样,我们需要完成 新增权限 / 删除权限 / 编辑权限
新增编辑权限的弹层
新增权限/编辑权限弹层
确定
取消
新增,编辑,删除权限点
新增/删除/编辑逻辑
import {
updatePermission, addPermission, getPermissionDetail, delPermission, getPermissionList } from '@/api/permission'
methods: {
// 删除操作
async delPermission(id) {
try {
await this.$confirm('确定要删除该数据吗')
await delPermission(id)
this.getPermissionList()
this.$message.success('删除成功')
} catch (error) {
console.log(error)
}
},
btnOK() {
this.$refs.perForm.validate().then(() => {
if (this.formData.id) {
return updatePermission(this.formData)
}
return addPermission(this.formData)
}).then(() => {
// 提示消息
this.$message.success('新增成功')
this.getPermissionList()
this.showDialog = false
})
},
btnCancel() {
this.formData = {
name: '', // 名称
code: '', // 标识
description: '', // 描述
type: '', // 类型 该类型 不需要显示 因为点击添加的时候已经知道类型了
pid: '', // 因为做的是树 需要知道添加到哪个节点下了
enVisible: '0' // 开启
}
this.$refs.perForm.resetFields()
this.showDialog = false
},
addPermission(pid, type) {
this.formData.pid = pid
this.formData.type = type
this.showDialog = true
},
async editPermission(id) {
// 根据获取id获取详情
this.formData = await getPermissionDetail(id)
this.showDialog = true
}
}
提交代码
本节任务
: 权限点管理页面开发
给角色分配权限
目标
: 完成给角色分配权限的业务
新建分配权限弹出层
在公司设置的章节中,我们没有实现分配权限的功能,在这里我们来实现一下
封装分配权限的api src/api/setting.js
// 给角色分配权限
export function assignPerm(data) {
return request({
url: '/sys/role/assignPrem',
method: 'put',
data
})
}
给角色分配权限弹出层
确定
取消
定义数据
showPermDialog: false, // 控制分配权限弹层的显示后者隐藏
defaultProps: {
label: 'name'
},
permData: [], // 专门用来接收权限数据 树形数据
selectCheck: [], // 定义一个数组来接收 已经选中的节点
roleId: null // 用来记录分配角色的id
点击分配权限
分配权限
给角色分配权限
分配权限/树形数据
import {
transListToTreeData } from '@/utils'
import {
getPermissionList } from '@/api/permission'
methods: {
// 点击分配权限
// 获取权限点数据 在点击的时候调用 获取权限点数据
async assignPerm(id) {
this.permData = tranListToTreeData(await getPermissionList(), '0') // 转化list到树形数据
this.roleId = id
// 应该去获取 这个id的 权限点
// 有id 就可以 id应该先记录下来
const {
permIds } = await getRoleDetail(id) // permIds是当前角色所拥有的权限点数据
this.selectCheck = permIds // 将当前角色所拥有的权限id赋值
this.showPermDialog = true
},
async btnPermOK() {
// 调用el-tree的方法
// console.log(this.$refs.permTree.getCheckedKeys())
await assignPerm({
permIds: this.$refs.permTree.getCheckedKeys(), id: this.roleId })
this.$message.success('分配权限成功')
this.showPermDialog = false
},
btnPermCancel() {
this.selectCheck = [] // 重置数据
this.showPermDialog = false
}
}
提交代码
本节任务
给角色分配权限
前端权限应用-页面访问和菜单
目标
: 在当前项目应用用户的页面访问权限
权限受控的主体思路
到了最关键的环节,我们设置的权限如何应用?
在上面的几个小节中,我们已经把给用户分配了角色, 给角色分配了权限,那么在用户登录获取资料的时候,会自动查出该用户拥有哪些权限,这个权限需要和我们的菜单还有路由有效结合起来
我们在路由和页面章节中,已经介绍过,动态权限其实就是根据用户的实际权限来访问的,接下来我们操作一下
在权限管理页面中,我们设置了一个标识, 这个标识可以和我们的路由模块进行关联,也就是说,如果用户拥有这个标识,那么用户就可以拥有这个路由模块,如果没有这个标识,就不能访问路由模块
用什么来实现?
vue-router提供了一个叫做addRoutes的API方法,这个方法的含义是动态添加路由规则
思路如下
新建Vuex中管理权限的模块
在主页模块章节中,我们将用户的资料设置到vuex中,其中便有权限数据,我们可以就此进行操作
我们可以在vuex中新增一个permission模块
src/store/modules/permission.js
// vuex的权限模块
import {
constantRoutes } from '@/router'
// vuex中的permission模块用来存放当前的 静态路由 + 当前用户的 权限路由
const state = {
routes: constantRoutes // 所有人默认拥有静态路由
}
const mutations = {
// newRoutes可以认为是 用户登录 通过权限所得到的动态路由的部分
setRoutes(state, newRoutes) {
// 下面这么写不对 不是语法不对 是业务不对
// state.routes = [...state.routes, ...newRoutes]
// 有一种情况 张三 登录 获取了动态路由 追加到路由上 李四登录 4个动态路由
// 应该是每次更新 都应该在静态路由的基础上进行追加
state.routes = [...constantRoutes, ...newRoutes]
}
}
const actions = {
}
export default {
namespaced: true,
state,
mutations,
actions
}
在Vuex管理模块中引入permisson模块
import permission from './modules/permission'
const store = new Vuex.Store({
modules: {
// 子模块 $store.state.app.
// mapGetters([])
app,
settings,
user,
permission
},
getters
})
Vuex筛选权限路由
OK, 那么我们在哪将用户的标识和权限进行关联呢 ?
我们可以在这张图中,进一步的进行操作
访问权限的数据在用户属性**menus
中,menus
**中的标识该怎么和路由对应呢?
可以将路由模块的根节点**name
**属性命名和权限标识一致,这样只要标识能对上,就说明用户拥有了该权限
这一步,在我们命名路由的时候已经操作过了
接下来, vuex的permission中提供一个action,进行关联
import {
asyncRoutes, constantRoutes } from '@/router'
const actions = {
// 筛选权限路由
// 第二个参数为当前用户的所拥有的菜单权限
// menus: ["settings","permissions"]
// asyncRoutes是所有的动态路由
// asyncRoutes [{path: 'setting',name: 'setting'},{}]
filterRoutes(context, menus) {
const routes = []
// 筛选出 动态路由中和menus中能够对上的路由
menus.forEach(key => {
// key就是标识
// asyncRoutes 找 有没有对象中的name属性等于 key的 如果找不到就没权限 如果找到了 要筛选出来
routes.push(...asyncRoutes.filter(item => item.name === key)) // 得到一个数组 有可能 有元素 也有可能是空数组
})
// 得到的routes是所有模块中满足权限要求的路由数组
// routes就是当前用户所拥有的 动态路由的权限
context.commit('setRoutes', routes) // 将动态路由提交给mutations
return routes // 这里为什么还要return state数据 是用来 显示左侧菜单用的 return 是给路由addRoutes用的
}
权限拦截出调用筛选权限Action
在拦截的位置,调用关联action, 获取新增routes,并且addRoutes
// 权限拦截在路由跳转 导航守卫
import router from '@/router'
import store from '@/store' // 引入store实例 和组件中的this.$store是一回事
import nprogress from 'nprogress' // 引入进度条
import 'nprogress/nprogress.css' // 引入进度条样式
// 不需要导出 因为只需要让代码执行即可
// 前置守卫
// next是前置守卫必须必须必须执行的钩子 next必须执行 如果不执行 页面就死了
// next() 放过
// next(false) 跳转终止
// next(地址) 跳转到某个地址
const whiteList = ['/login', '/404'] // 定义白名单
router.beforeEach(async(to, from, next) => {
nprogress.start() // 开启进度条的意思
if (store.getters.token) {
// 只有有token的情况下 才能获取资料
// 如果有token
if (to.path === '/login') {
// 如果要访问的是 登录页
next('/') // 跳到主页 // 有token 用处理吗?不用
} else {
// 只有放过的时候才去获取用户资料
// 是每次都获取吗
// 如果当前vuex中有用户的资料的id 表示 已经有资料了 不需要获取了 如果没有id才需要获取
if (!store.getters.userId) {
// 如果没有id才表示当前用户资料没有获取过
// async 函数所return的内容 用 await就可以接收到
const {
roles } = await store.dispatch('user/getUserInfo')
// 如果说后续 需要根据用户资料获取数据的话 这里必须改成 同步
// 筛选用户的可用路由
// actions中函数 默认是Promise对象 调用这个对象 想要获取返回的值话 必须 加 await或者是then
// actions是做异步操作的
const routes = await store.dispatch('permission/filterRoutes', roles.menus)
// routes就是筛选得到的动态路由
// 动态路由 添加到 路由表中 默认的路由表 只有静态路由 没有动态路由
// addRoutes 必须 用 next(地址) 不能用next()
router.addRoutes(routes) // 添加动态路由到路由表 铺路
// 添加完动态路由之后
next(to.path) // 相当于跳到对应的地址 相当于多做一次跳转 为什么要多做一次跳转
// 进门了,但是进门之后我要去的地方的路还没有铺好,直接走,掉坑里,多做一次跳转,再从门外往里进一次,跳转之前 把路铺好,再次进来的时候,路就铺好了
} else {
next()
}
}
} else {
// 没有token的情况下
if (whiteList.indexOf(to.path) > -1) {
// 表示要去的地址在白名单
next()
} else {
next('/login')
}
}
nprogress.done() // 解决手动切换地址时 进度条不关闭的问题
})
// 后置守卫
router.afterEach(() => {
nprogress.done() // 关闭进度条
})
静态路由动态路由解除合并
注意: 这里有个非常容易出问题的位置,当我们判断用户是否已经添加路由的前后,不能都是用next(),
在添加路由之后应该使用 next(to.path), 否则会使刷新页面之后 权限消失,这属于一个vue-router的已知缺陷
同时,不要忘记,我们将原来的静态路由 + 动态路由合体的模式 改成 只有静态路由 src/router/index.js
此时,我们已经完成了权限设置的一半, 此时我们发现左侧菜单失去了内容,这是因为左侧菜单读取的是固定的路由,我们要把它换成实时的最新路由
在**src/store/getters.js
**配置导出routes
const getters = {
sidebar: state => state.app.sidebar,
device: state => state.app.device,
token: state => state.user.token,
name: state => state.user.userInfo.username, // 建立用户名称的映射
userId: state => state.user.userInfo.userId, // 建立用户id的映射
companyId: state => state.user.userInfo.companyId, // 建立用户的公司Id映射
routes: state => state.permission.routes // 导出当前的路由
}
export default getters
在左侧菜单组件中, 引入routes
computed: {
...mapGetters([
'sidebar', 'routes'
]),
OK,到现在为止,我们已经可以实现不同用户登录的时候,菜单是动态的了
提交代码
本节任务
前端权限应用-页面访问和菜单
登出时,重置路由权限和 404问题
目标: 处理当登出页面时,路由不正确的问题
上一小节,我们看似完成了访问权限的功能,实则不然,因为当我们登出操作之后,虽然看不到菜单,但是用户实际上可以访问页面,直接在地址栏输入地址就能访问
这是怎么回事?
这是因为我们前面在addRoutes的时候,一直都是在加,登出的时候,我们并没有删,也没有重置,也就是说,我们之前加的路由在登出之后一直在,这怎么处理?
大家留意我们的router/index.js文件,发现一个重置路由方法
// 重置路由
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher // 重新设置路由的可匹配路径
}
没错,这个方法就是将路由重新实例化,相当于换了一个新的路由,之前**加的路由
**自然不存在了,只需要在登出的时候, 处理一下即可
// 登出的action
lgout(context) {
// 删除token
context.commit('removeToken') // 不仅仅删除了vuex中的 还删除了缓存中的
// 删除用户资料
context.commit('removeUserInfo') // 删除用户信息
// 重置路由
resetRouter()
// 还有一步 vuex中的数据是不是还在
// 要清空permission模块下的state数据
// vuex中 user子模块 permission子模块
// 子模块调用子模块的action 默认情况下 子模块的context是子模块的
// 父模块 调用 子模块的action
context.commit('permission/setRoutes', [], {
root: true })
// 子模块调用子模块的action 可以 将 commit的第三个参数 设置成 { root: true } 就表示当前的context不是子模块了 而是父模块
}
除此之外,我们发现在页面刷新的时候,本来应该拥有权限的页面出现了404,这是因为404的匹配权限放在了静态路由,而动态路由在没有addRoutes之前,找不到对应的地址,就会显示404,所以我们需要将404放置到动态路由的最后
src/permission.js
router.addRoutes([...routes, {
path: '*', redirect: '/404', hidden: true }]) // 添加到路由表
提交代码
功能权限应用
目标: 实现功能权限的应用
功能权限的受控思路
上小节中,当我们拥有了一个模块,一个页面的访问权限之后,页面中的某些功能,用户可能有,也可能没有,这就是功能权限
这就是上小节,查询出来的数据中的**points
**
比如,我们想对员工管理的删除功能做个权限怎么做?
首先需要在员工管理的权限点下, 新增一个删除权限点,启用
我们要做的就是看看用户,是否拥有point-user-delete这个point,有就可以让删除能用,没有就隐藏或者禁用
使用Mixin技术将检查方法注入
所以,我们可以采用一个新的技术 mixin(混入)来让所有的组件可以拥有一个公共的方法
src/mixin/checkPermission.js
import store from '@/store'
export default {
methods: {
checkPermission(key) {
const {
userInfo } = store.state.user
if (userInfo.roles.points && userInfo.roles.points.length) {
return userInfo.roles.points.some(item => item === key)
}
return false
}
}
}
在员工组件中检查权限点
查看
此时,可以通过配置权限的方式,检查权限的可用性了
提交代码
全模块集成
目标: 将其他业务模块代码集成到该项目中
到目前为止,我们已经完成了一个基本项目框架 + 组织架构 + 公司 + 员工 + 权限的 业务联调, 时间关系,我们不可能将所有的业务都去编写一遍,这里提供大家 其余模块的集成代码,最终的目的是让大家得到一个完成的业务模块
要集成的模块业务,包括工资模块,社保模块,考勤模块,审批模块
在我们提供的资源集成模块中,我们提供了四个模块的**路由
/页面
/api
*, 按照下面的路径拷贝即可
路由 => src/router/modules
页面 => src/views
api => src/api
除此之外,我们需要将 router/modules/user.js
导入到静态路由中,因为这个模块是所有模块都可以访问的
最终,我们将得到一个较为完整的系统业务。
首页的页面结构
目标
: 实现系统首页的页面结构
目前,我们的页面还剩下首页,这里我们可以按照如图实现一下的结构
首页页面结构,src/views/dashboard/index.vue
通过上面的代码,我们得到了如下的页面
大家发现,我们预留了**工作日历
和绩效指数
**两个组件,我们会在后续的组件中进行开发
提交代码
首页用户资料显示
目标
:将首页的信息换成真实的用户资料
直接获取Vuex的用户资料即可
在 vue视图中绑定
早安,{
{ userInfo.username }},祝你开心每一天!
{
{ userInfo.username }} | {
{ userInfo.companyName }}-{
{ userInfo.departmentName }}
除此之外,当我们加载图片失败的时候,图片地址存在,但是却不能显示,之前我们封装的图片错误指令可以应用
工作日历组件封装
**目标
**封装一个工作日历组件在首页中展示
新建工作日历组件结构
工作日历的要求很简单,显示每个月的日期,可以设定日期的范围
我们可以基于Element组件el-calendar进行封装
代码如下 src/views/dashboard/components/work-calendar.vue
{
{ item }}
{
{ item }}
{
{ data.day | getDay }}
休
实现工作日历逻辑
export default {
filters: {
getDay(value) {
const day = value.split('-')[2]
return day.startsWith('0') ? day.substr(1) : day
}
},
props: {
startDate: {
type: Date,
default: () => new Date()
}
},
data() {
return {
currentMonth: null, // 当前月份
currentYear: null, // 当前年份
currentDate: null,
yearList: []
}
},
// 初始化事件
created() {
// 处理起始时间
// 组件要求起始时间必须是 周一开始 以一个月为开始
this.currentMonth = this.startDate.getMonth() + 1
this.currentYear = this.startDate.getFullYear()
// 根据当前的年 生成可选年份 前后各加5年
this.yearList = Array.from(Array(11), (value, index) => this.currentYear + index - 5 )
// 计算 当年当月的第一个周一 再加上 四周之后的一个月月份
this.dateChange()
},
methods: {
// 是否是休息日
isWeek(value) {
return value.getDay() === 6 || value.getDay() === 0
},
// 年月份改变之后
dateChange() {
const year = this.currentYear
const month = this.currentMonth
this.currentDate = new Date(`${
year}-${
month}-1`) // 以当前月的1号为起始
}
}
}
在主页中应用
提交代码
封装雷达图图表显示在首页
目标
:封装一个echarts中的雷达图表显示在首页的绩效指数的位置
了解雷达图
封装雷达图插件
首页中,还有一个绩效指数的位置需要放置一个雷达图的图表,我们可以采用百度的echarts进行封装
第一步, 安装echarts图表
$ npm i echarts
echarts是一个很大的包,里面包含了众多图形,假设我们只使用雷达图,可以做按需加载
第二步, 新建雷达图组件,src/views/dashboard/components/radar.vue
我们得到一个雷达图,对绩效指数进行统计
注意
:相关数据的缺失,所以这里我们进行的是模拟数据
在主页中引入使用
import Radar from './components/radar'
审批流程业务的基本介绍
什么是审批流程
提交一个离职审批
目标
: 提交一个离职的审批,并完成业务流转
离职弹层
确定
取消
显示弹层
加班离职
加班数据及校验
showDialog: false,
ruleForm: {
exceptTime: '',
reason: '',
processKey: 'process_dimission', // 特定的审批
processName: '离职'
},
rules: {
exceptTime: [{
required: true, message: '离职时间不能为空' }],
reason: [{
required: true, message: '离职原因不能为空' }]
}
提交审批逻辑
import {
startProcess } from '@/api/approvals'
methods: {
btnOK() {
this.$refs.ruleForm.validate(async validate => {
if (validate) {
const data = {
...this.ruleForm, userId: this.userInfo.userId }
await startProcess(data)
this.$message.success('提交流程成功')
this.btnCancel()
}
})
},
btnCancel() {
this.showDialog = false
this.$refs.ruleForm.resetFields()
this.ruleForm = {
exceptTime: '',
reason: '',
processKey: 'process_dimission', // 特定的审批
processName: '离职'
}
}
}
配置审批列表的导航
审批列表
我的信息
完成该流程的审批和流转
注意: 审批接口中的同意接口存在一定问题,可以测试 提交 /撤销 驳回等操作
提交代码
全屏插件的引用
目标:实现页面的全屏功能
全屏功能可以借助一个插件来实现
第一步,安装全局插件screenfull
$ npm i screenfull
第二步,封装全屏显示的插件·· src/components/ScreenFull/index.vue
第三步,全局注册该组件 src/components/index.js
import ScreenFull from './ScreenFull'
Vue.component('ScreenFull', ScreenFull) // 注册全屏组件
第四步,放置于**layout/navbar.vue
**中
.right-menu-item {
vertical-align: middle;
}
提交代码
本节任务
: 实现页面的全屏功能
动态主题的设置
目标
: 实现动态主题的设置
我们想要实现在页面中实时的切换颜色,此时页面的主题可以跟着设置的颜色进行变化
简单说明一下它的原理: element-ui 2.0 版本之后所有的样式都是基于 SCSS 编写的,所有的颜色都是基于几个基础颜色变量来设置的,所以就不难实现动态换肤了,只要找到那几个颜色变量修改它就可以了。 首先我们需要拿到通过 package.json
拿到 element-ui 的版本号,根据该版本号去请求相应的样式。拿到样式之后将样色,通过正则匹配和替换,将颜色变量替换成你需要的,之后动态添加 style
标签来覆盖原有的 css 样式。
第一步, 封装颜色选择组件 ThemePicker
代码地址:@/components/ThemePicker。
注意:本章节重点在于集成,内部的更换主题可以先不用关心。
实现代码
注册代码
import ThemePicker from './ThemePicker'
Vue.component('ThemePicker', ThemePicker)
第二步, 放置于**layout/navbar.vue
**中
提交代码
多语言实现
**目标
**实现国际化语言切换
初始化多语言包
本项目使用国际化 i18n 方案。通过 vue-i18n而实现。
第一步,我们需要首先国际化的包
$ npm i vue-i18n
第二步,需要单独一个多语言的实例化文件 src/lang/index.js
import Vue from 'vue' // 引入Vue
import VueI18n from 'vue-i18n' // 引入国际化的包
import Cookie from 'js-cookie' // 引入cookie包
import elementEN from 'element-ui/lib/locale/lang/en' // 引入饿了么的英文包
import elementZH from 'element-ui/lib/locale/lang/zh-CN' // 引入饿了么的中文包
Vue.use(VueI18n) // 全局注册国际化包
export default new VueI18n({
locale: Cookie.get('language') || 'zh', // 从cookie中获取语言类型 获取不到就是中文
messages: {
en: {
...elementEN // 将饿了么的英文语言包引入
},
zh: {
...elementZH // 将饿了么的中文语言包引入
}
}
})
上面的代码的作用是将Element的两种语言导入了
第三步,在main.js中对挂载 i18n的插件,并设置element为当前的语言
// 设置element为当前的语言
Vue.use(ElementUI, {
i18n: (key, value) => i18n.t(key, value)
})
new Vue({
el: '#app',
router,
store,
i18n,
render: h => h(App)
})
引入自定义语言包
此时,element已经变成了zh,也就是中文,但是我们常规的内容怎么根据当前语言类型显示?
这里,针对英文和中文,我们可以提供两个不同的语言包 src/lang/zh.js , src/lang/en.js
该语言包,我们已经在资源中提供
第四步,在index.js中同样引入该语言包
import customZH from './zh' // 引入自定义中文包
import customEN from './en' // 引入自定义英文包
Vue.use(VueI18n) // 全局注册国际化包
export default new VueI18n({
locale: Cookie.get('language') || 'zh', // 从cookie中获取语言类型 获取不到就是中文
messages: {
en: {
...elementEN, // 将饿了么的英文语言包引入
...customEN
},
zh: {
...elementZH, // 将饿了么的中文语言包引入
...customZH
}
}
})
在左侧菜单中应用多语言包
自定义语言包的内容怎么使用?
第五步,在左侧菜单应用
当我们全局注册i18n的时候,每个组件都会拥有一个**$t
**的方法,它会根据传入的key,自动的去寻找当前语言的文本,我们可以将左侧菜单变成多语言展示文本
layout/components/SidebarItem.vue
注意:当文本的值为嵌套时,可以通过**$t(key1.key2.key3...)
**的方式获取
现在,我们已经完成了多语言的接入,现在封装切换多语言的组件
封装多语言插件
第六步,封装多语言组件 src/components/lang/index.vue
中文
en
第七步,在Navbar组件中引入
最终效果
提交代码
tab页的视图引入
目标
: 实现tab页打开路由的功能
当前我们实现的打开页面,看到一个页面之后,另一个页面就会关闭,为了显示更加有效率,我们可以引入多页签组件
多页签的组件的代码过于繁杂,开发实际需要的是集成和调用能力,所以我们只是将开发好的组件集成到当前的功能项中即可。
在资源目录中,**多页签
**目录下放置的是 组件和vuex模块
第一步,将组件TagsView目录放置到**src/components
** , 并全局注册
import TagsView from './TagsView'
Vue.component('TagsView', TagsView)
第二步,将Vuex模块**tagsView.js
**放置到 src/store/modules
并在store中引入该模块
import tagsView from './modules/tagsView'
const store = new Vuex.Store({
modules: {
app,
settings,
user,
permission,
tagsView
},
getters
})
第三步,在**src/layout/Index.vue
**中引入该组件
效果如下