HR-saas中台管理项目-基于vue-element-admin-业务模块页面

本节任务:完成业务路由页面的整理

业务模块页面的快速搭建

目标: 快速搭建人资项目的常规业务模块

新建模块的页面和路由文件

截止到现在,我们已经完成了一个中台系统的基本轮廓,如图

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第1张图片

接下来,我们可以将人力资源需要做的模块快速搭建相应的页面和路由

├── 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] // 临时合并所有的路由
})

通过上面的操作,我们将静态路由和动态路由进行了合并

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第2张图片

当我们合并权限完成,我们惊奇的发现页面效果已经左侧的导航菜单 =》 路由页面

这是之前基础模板中对于左侧导航菜单的封装

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第3张图片

提交代码

本节任务: 将静态路由和动态路由临时合并,形成左侧菜单

左侧菜单的显示逻辑,设置菜单图标

目标 解析左侧菜单的显示逻辑, 设置左侧导航菜单的图标内容

上小节中,我们集成了路由,菜单就显示内容了,这是为什么 ?

阅读左侧菜单代码

我们发现如图的逻辑

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第4张图片

由于,该项目不需要二级菜单的显示,所以对代码进行一下处理,只保留一级菜单路由

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

image-20200720010016813

按照对应的icon设置图标

本节任务: 理解左侧菜单的生成逻辑,并设置左侧菜单的图标

组织架构树形结构布局

目标:使用element-UI组件布局组织架构的基本布局

认识组织架构

组织架构产品prd

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第5张图片

一个企业的组织架构是该企业的灵魂,组织架构多常采用树形金字塔式结构,本章节,我们布局出页面的基本结构

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第6张图片

实现组织架构的头部内容

首先实现头部的结构,采用element的行列布局

   
        
        
          
            江苏传智播客教育科技股份有限公司
          
          
            
              
              负责人
              
                
                
                  
                    操作
                  
                  
                  
                    添加子部门
                  
                
              
            
          
        
      

样式




HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第7张图片

树形组件认识

接下来,实现树形的结构,采用element的**tree组件**, 如图效果

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第8张图片

树形组件属性

参数 说明 类型 可选值 默认值
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'
            }]
          }]
        }]

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第9张图片

实现树形的静态组织架构

由此,我们首先实现静态数据的组织架构

 
 
export default {
  data() {
    return {
      defaultProps: {
        label: 'name'
      },
      departs: [{ name: '总裁办', children: [{ name: '董事会' }] },
        { name: '行政部' }, { name: '人事部' }]
    }
  }
}

接下来,对每个层级节点增加显示内容,此时需要用到tree的插槽

 
          
          
          
            
              
              {
    { data.name }}
            
            
              
                {
    { data.manager }}
                
                  
                  
                    
                    操作
                      
                    
                    
                    
                      
                      添加子部门
                      编辑部门
                      删除部门

                    
                  
                
              

            
            
                  


最终形成静态结构效果

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第10张图片

提交代码

本节任务:完成树形结构的显示

将树形的操作内容单独抽提成组件

目标: 将树形的操作内容单独抽提成组件

封装单独的树操作栏组件

通过第一个章节,我们发现,树形的顶级内容实际和子节点的内容是一致的,此时可以将该部分抽提成一个组件,节省代码

组件 src/views/departments/components/tree-tools.vue







在组织架构中应用操作栏组件

接下来,在**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判断显示

     
          编辑部门
         删除部门

通过封装,代码看上去更加紧凑,简洁,这就是封装的魅力

提交代码

本节任务:将树形内容单独抽提组件

获取组织架构数据,并进行树形处理

**目标**获取真实的组织架构数据,并将其转化成树形数据显示在页面上

封装API接口,获取组织架构数据

现在基本的静态结构已经形成,接下来需要获取真实的数据

首先,封装获取组织架构的请求 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)
    }
  }
}

将数组数据转化成树形结构

然后,我们需要将列表型的数据,转化成树形数据,这里需要用到递归算法

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第11张图片

封装一个工具方法,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

其中的交互设计如下

image-20200828162901390

设计要求




点击新增子部门显示弹层组件

然后,我们需要用属性控制组件的显示或者隐藏

  // 需要传入一个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
    }


提交代码

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第12张图片

本节任务:新增部门功能-建立组件

完成新增部门的规则校验

目标 完成新增部门功能的规则校验和数据提交部分

完成新增表单的基本校验条件

部门名称(name):必填 1-50个字符 / 同级部门中禁止出现重复部门

部门编码(code):必填 1-50个字符 / 部门编码在整个模块中都不允许重复

部门负责人(manager):必填

部门介绍 ( introduce):必填 1-300个字符

定义数据结构

  formData: {
     
        name: '', // 部门名称
        code: '', // 部门编码
        manager: '', // 部门管理者
        introduce: '' // 部门介绍
  },


完成表单校验需要的前置条件

  • el-form配置model和rules属性
  • el-form-item配置prop属性
  • 表单进行v-model双向绑定

配置新增表单的基本校验规则

根据这些要求,校验规则

  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个字符' }]
      }


处理首部内容的pid数据

需要注意:在最根级的**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修饰符关闭新增弹层

这里我们学习一个新的技巧,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事件

  


本节任务 新增功能-提交-取消-关闭

编辑部门功能实现数据回写

目标:实现编辑部门的功能

点击编辑弹出层,记录当前节点

编辑部门功能实际上和新增窗体采用的是一个组件,只不过我们需要将新增场景变成编辑场景

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第13张图片

首先点击编辑部门时, 调用父组件编辑方法 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()
    }


至此,整个组织架构, 我们完成了,组织架构读取 / 新增部门 / 删除部门 / 编辑部门

如图

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第14张图片

提交代码

**本节任务**编辑部门功能实现

给数据获取添加加载进度条

目标 给当前组织架构添加加载进度条

由于获取数据的延迟性,为了更好的体验,可以给页面增加一个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
}


建立公司角色页面的基本结构

**目标**建立公司页面的基本结构

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第15张图片

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第16张图片

根据以上的结构,我们采用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
    }


新增按钮注册事件

新增角色


提交代码

本节任务 新增角色功能

总结

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第17张图片

我们完成了公司中角色管理的部分,但是并没有完成分配权限的部分,该部门会在权限设计和管理的部门重点提及

封装一个通用的工具栏

目标:封装一个通用的工具栏供大家使用

通用工具栏的组件结构

在后续的业务开发中,经常会用到一个类似下图的工具栏,作为公共组件,进行一下封装

image-20200723223704401

组件 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



HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第18张图片

提交代码

本节任务:员工列表页面的基本布局和结构

员工列表数据请求和分页加载

**目标**实现员工数据的加载和分页请求

首先,封装员工的加载请求 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
    }
  }
}

绑定表格

      
        
          
          
          
          
          
          
          
          
            
          
        
        
        
          
        
      

提交代码

**本节任务**员工列表数据请求和分页加载

员工列表中的数据进行格式化

目标:将列表中的内容进行格式化

利用列格式化属性处理聘用形式

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第19张图片

上小节中,列表中的聘用形式/入职时间账户状态需要进行显示内容的处理

那么聘用形式中1代表什么含义,这实际上是我们需要的枚举数据,该数据的存放文件位于我们提供的**资源/枚举中,可以将枚举下的文件夹放于src/api**文件夹下

针对聘用形式,可以使用el-table-columnformatter属性进行设置

  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 : '未知'
    }

过滤器解决时间格式的处理

针对入职时间,我们可以采用作用域插槽进行处理

 
            
  

问题来了,过滤器从哪里呢?

在**资源/过滤器中,我们提供了若干工具方法,我们可以将其转化成过滤器,首先将其拷贝到src**

在**main.js**中将工具方法转化成过滤器

import * as filters from '@/filters' // 引入工具类
// 注册全局的过滤器
Object.keys(filters).forEach(key => {
     
  // 注册过滤器
  Vue.filter(key, filters[key])
})

好了,现在可以愉快的用过滤器的方式使用工具类的方法了

       
            
            
       

最后一项,账户状态,可以用开关组件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)
      }
    }

提交代码

本节任务: 删除员工功能

新增员工功能-弹层-校验-部门

目标:实现新增员工的功能

新建员工弹层组件

当我们点击新增员工时,我们需要一个类似的弹层

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第20张图片

类似**组织架构**的组件,同样新建一个弹层组件 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数据的文件

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第21张图片

首先封装一个类似的组件,首先需要注意的是,类似功能,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-saverscript-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 提供的导出方法中有 multiHeadermerges 的参数

参数 说明 类型 可选值 默认值
multiHeader 复杂表头的部分 Array / [[]]
merges 需要合并的部分 Array / []

multiHeader里面是一个二维数组,里面的一个元素是一行表头,假设你想得到一个如图的结构

image-20201014152302636

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]]))
      // 需要处理时间格式问题
    }


提交代码

**本节任务**实现将员工数据导出功能

员工详情页创建和布局

目标:创建员工详情的主要布局页面和基本布局

详情页的基本布局和路由

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第22张图片

建立详情页路由

{
     
    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)
      }
    }
  }
}


绑定表单数据

 
            
              
                
              
              
                
              
              
                更新
              
            


提交代码

个人组件和岗位组件封装

封装个人详情组件

我们将员工个人信息分为三部分,账户,个人, 岗位,这个小节我们对个人组件和岗位组件进行封装

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第23张图片

封装个人组件 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

由于上课的开发的特殊性,我们不希望把所有的图片都上传到我们自己的官方服务器上,这里我们可以采用一个腾讯云的图片方案

image-20200805200831035

上边图的意思就是说,我们找一个可以免费上传图片的服务器,帮我们**代管图片,我们在自己的数据库里只保存一个地址就行, 这其实也是很多项目的处理方案,会有一个公共的文件服务器**

第一步,我们必须先拥有一个腾迅云的开发者账号(小心腾讯云的广告电话)

请按照腾讯云的注册方式,注册自己的账号

第二步,实名认证

选择个人账户

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第24张图片

填写个人身份信息

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第25张图片

下一步,扫描二维码授权

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第26张图片

手机端授权

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第27张图片

点击领取免费产品

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第28张图片

选择对象存储COS

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第29张图片

我们免费拥有**6个月的50G流量**的对象存储空间使用权限,足够我们上传用户头像的使用了

点击0元试用,开通服务

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第30张图片

到这一步,账号的部分就操作完毕,接下来,我们需要来创建一个存储图片的存储桶

登录 对象存储控制台 ,创建存储桶。设置存储桶的权限为 公有读,私有写

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第31张图片

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第32张图片

设置cors规则

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第33张图片

AllowHeader 需配成*,如下图所示。

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第34张图片

因为我们本身没有域名,所以这里设置成*****,仅限于测试,正式环境的话,这里需要配置真实的域名地址

到这里,我们的腾讯云存储桶就设置好了。

封装上传图片组件-上传组件需求分析

目标 梳理整个的上传过程

初始化cos对象参数

名称 描述
SecretId 开发者拥有的项目身份识别 ID,用以身份认证,可在 API 密钥管理 页面获取
SecretKey 开发者拥有的项目身份密钥,可在 API 密钥管理 页面获取

注意,上述的参数我们在本次开发过程中,直接将参数放置在前端代码中存储,但是腾讯云本身是不建议这么做的,因为**敏感信息**放在前端很容易被捕获,由于我们本次是测试研发,所以这个过程可以忽略

正确的做法应该是,通过网站调用接口换取敏感信息

相关文档

实例化 上传sdk

var cos = new COS({
     
    SecretId: 'COS_SECRETID', // 身份识别 ID
    SecretKey: 'COS_SECRETKEY', // 身份密钥
});


到目前为止,我们上传图片准备的内容就已经OK,接下来,我们在**src/componets** 新建一个**ImageUpload** 组件

该组件需要满足什么要求呢?

  1. 可以显示传入的图片地址
  2. 可以删除传入的图片地址
  3. 可以上传图片到云服务器
  4. 上传到腾讯云之后,可以返回图片地址,显示
  5. 上传成功之后,可以回调成功函数

这个上传组件简单吗?

no ! ! !

看似需求很明确,但是它真正的实现很复杂,我们通过一个图来看一下

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第35张图片

从上图中,我们可以看到,实际上是有两种场景的,本地场景和已经上传的场景

下个章节,针对这个场景我们进行开发

封装上传组件-代码实现

**目标**实现上传组件的代码部分

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。

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第36张图片

实现代码

    // 进行上传操作
    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('保存基础信息成功')
    }


提交代码

本节任务 : 在员工详情中应用上传组件

员工列表显示图片

目标:在员工列表中心显示图片

员工的头像可以在列表项中添加一列来进行显示


            



我们尝试用之前的指令来处理图片的异常问题,但是发现只有两三张图片能显示

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第37张图片

这是因为有的员工的头像的地址为空,给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
  }
}



这样我们可以看到每个用户的头像了,如果没有头像则显示默认图片

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第38张图片

任务:员工列表显示图片

图片地址生成二维码

目标 将图片地址生成二维码显示

我们想完成这样一个功能,当我们拥有头像地址时,将头像地址生成一个二维码,用手机扫码来访问

首先,需要安装生成二维码的插件

$ 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)]

打印员工信息

目标 完成个人信息和工作信息的打印功能

新建打印页面及路由

创建页面组件










该页面内容实际上就是读取个人和详情的接口数据,根据传入的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'
   }


最终,我们看到的效果

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第39张图片

提交代码

**本节任务**打印员工信息

权限设计-RBAC的权限设计思想

首先,我们先了解下什么是传统的权限设计

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第40张图片

从上面的图中,我们发现,传统的权限设计是对每个人进行单独的权限设置,但这种方式已经不适合目前企业的高效管控权限的发展需求,因为每个人都要单独去设置权限

基于此,RBAC的权限模型就应运而生了,RBAC(Role-Based Access control) ,也就是基于角色的权限分配解决方案,相对于传统方案,RBAC提供了中间层Role(角色),其权限模式如下

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第41张图片

RBAC实现了用户和权限点的分离,想对某个用户设置权限,只需要对该用户设置相应的角色即可,而该角色就拥有了对应的权限,这样一来,权限的分配和设计就做到了极简,高效,当想对用户收回权限时,只需要收回角色即可,接下来,我们就在该项目中实施这一设想

给分配员工角色

**目标**在员工管理页面,分配角色

新建分配角色窗体

在上一节章节中,员工管理的角色功能,我们并没有实现,此章节我们实现给员工分配角色

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第42张图片

从上图中,可以看出,用户和角色是**1对多**的关系,即一个用户可以拥有多个角色,比如公司的董事长可以拥有总经理和系统管理员一样的角色

首先,新建分配角色窗体 assign-role.vue

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第43张图片







获取角色列表和当前用户角色

获取所有角色列表

  
    
      
        {
    {
          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权限多见于在后端进行拦截,所以我们这一版本只做**页面访问按钮操作授权/**

由此,我们可以根据业务需求设计权限管理页面

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第44张图片

完成权限页面结构 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
    }
}



提交代码

本节任务 给角色分配权限

前端权限应用-页面访问和菜单

目标: 在当前项目应用用户的页面访问权限

权限受控的主体思路

到了最关键的环节,我们设置的权限如何应用?

在上面的几个小节中,我们已经把给用户分配了角色, 给角色分配了权限,那么在用户登录获取资料的时候,会自动查出该用户拥有哪些权限,这个权限需要和我们的菜单还有路由有效结合起来

我们在路由和页面章节中,已经介绍过,动态权限其实就是根据用户的实际权限来访问的,接下来我们操作一下

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第45张图片

在权限管理页面中,我们设置了一个标识, 这个标识可以和我们的路由模块进行关联,也就是说,如果用户拥有这个标识,那么用户就可以拥有这个路由模块,如果没有这个标识,就不能访问路由模块

用什么来实现?

vue-router提供了一个叫做addRoutes的API方法,这个方法的含义是动态添加路由规则

思路如下

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第46张图片

新建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, 那么我们在哪将用户的标识和权限进行关联呢 ?

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第47张图片

我们可以在这张图中,进一步的进行操作

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第48张图片

访问权限的数据在用户属性**menus中,menus**中的标识该怎么和路由对应呢?

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第49张图片

可以将路由模块的根节点**name**属性命名和权限标识一致,这样只要标识能对上,就说明用户拥有了该权限

这一步,在我们命名路由的时候已经操作过了

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第50张图片

接下来, 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

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第51张图片

此时,我们已经完成了权限设置的一半, 此时我们发现左侧菜单失去了内容,这是因为左侧菜单读取的是固定的路由,我们要把它换成实时的最新路由

在**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**

比如,我们想对员工管理的删除功能做个权限怎么做?

首先需要在员工管理的权限点下, 新增一个删除权限点,启用

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第52张图片

我们要做的就是看看用户,是否拥有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 导入到静态路由中,因为这个模块是所有模块都可以访问的

最终,我们将得到一个较为完整的系统业务。

首页的页面结构

目标: 实现系统首页的页面结构

目前,我们的页面还剩下首页,这里我们可以按照如图实现一下的结构

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第53张图片

首页页面结构,src/views/dashboard/index.vue








通过上面的代码,我们得到了如下的页面

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第54张图片

大家发现,我们预留了**工作日历绩效指数**两个组件,我们会在后续的组件中进行开发

提交代码

首页用户资料显示

目标:将首页的信息换成真实的用户资料

直接获取Vuex的用户资料即可



在 vue视图中绑定

早安,{ { userInfo.username }},祝你开心每一天!

{ { userInfo.username }} | { { userInfo.companyName }}-{ { userInfo.departmentName }}

除此之外,当我们加载图片失败的时候,图片地址存在,但是却不能显示,之前我们封装的图片错误指令可以应用



工作日历组件封装

**目标**封装一个工作日历组件在首页中展示

新建工作日历组件结构

工作日历的要求很简单,显示每个月的日期,可以设定日期的范围

我们可以基于Element组件el-calendar进行封装

代码如下 src/views/dashboard/components/work-calendar.vue








实现工作日历逻辑

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








我们得到一个雷达图,对绩效指数进行统计

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第55张图片

注意:相关数据的缺失,所以这里我们进行的是模拟数据

在主页中引入使用

import Radar from './components/radar'


审批流程业务的基本介绍

什么是审批流程

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第56张图片

提交一个离职审批

目标: 提交一个离职的审批,并完成业务流转

离职弹层

    
    
      
        
        
          
        
        
          
        
      
      
        
          确定
          取消
        
      
    

显示弹层

加班离职

加班数据及校验

      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







第七步,在Navbar组件中引入

 
      
      
      
      
      

最终效果

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第57张图片

提交代码

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**中引入该组件




效果如下

HR-saas中台管理项目-基于vue-element-admin-业务模块页面_第58张图片

你可能感兴趣的:(vue)