# 员工管理模块

需求:管理员工相关的信息(员工信息的增删改查,支持批量导入,支持角色授权等)

# 通用工具栏的组件结构

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

image-20200723223704401

组件 src/components/PageTools/index.vue

<template>
  <el-card>
    <div class="page-tools">
      <!-- 左侧 -->
      <div class="left">
        <div class="tips">
          <i class="el-icon-info" />
          <span>本月: 社保在缴 公积金在缴</span>
		</div>
      </div>
      <div class="right">
        <!-- 右侧 -->
        <slot name="right" />
      </div>
    </div>
  </el-card>
</template>

<script>
export default {
}
</script>

<style lang="scss" scoped>
.page-tools {
  display: flex;
  justify-content: space-between;
  align-items: center;
  .tips {
    line-height: 34px;
    padding: 0px 15px;
    border-radius: 5px;
    border: 1px solid rgba(145, 213, 255, 1);
    background: rgba(230, 247, 255, 1);
    i {
      margin-right: 10px;
      color: #409eff;
    }
  }
}
</style>
  • 通过全局插件的方式扩展全局组件(这样的话,其他组件中可以直接使用这个全局组件)
/*
  封装Vue插件
*/
import PageTools from '@/components/PageTools/index.vue'

export default {
  install(Vue, options) {
    // 扩展一个自定义指令,处理图片加载失败的情况
    // <img v-imgerror='default.png' src="a.png" alt=""/>
    Vue.directive('imgerror', {
      // bindings包含指令相关的参数信息
      inserted(el, bindings) {
        // console.dir(bindings)
        // 如何知道img标签图片加载失败了?
        el.onerror = () => {
          // 加载失败后触发该函数
          el.src = bindings.value || options.defaultImg
        }
      }
    })

    // 扩展全局组件
    Vue.component('page-tools', PageTools)
  }
}

效果图:

image-20210125112052353

# 具名插槽优化

左边右边的内容不是写死的, 而是通过插槽定制的, 且如果左侧插槽没有传, 那么左边的tips就没有必要显示了tips: 通过 $slots 可以拿到所有的插槽列表, $slot.插槽名 可以用于判断该插槽是否传值

<template>
  <el-card>
    <div class="page-tools">
      <!-- 左侧 -->
      <div class="left">
        <div v-if="$slots.left" class="tips">
          <i class="el-icon-info" />
          <slot name="left" />
        </div>
      </div>
      <!-- 右侧 -->
      <div class="right">
        <slot name="right" />
      </div>
    </div>
  </el-card>
</template>
  • 组件中使用示例
<template>
  <div class="employees-container">
    <div class="app-container">
      <page-tools>
        <template #left>
          <span>本月: 社保在缴 公积金在缴</span>
        </template>
        <template #right>
          <el-button type="primary" size="small">历史归档</el-button>
          <el-button type="primary" size="small">导出</el-button>
        </template>
      </page-tools>
    </div>
  </div>
</template>

总结:封装组件时,可以通过插槽定制组件的默认行为

  1. 普通默认插槽
  2. 具名插槽
  3. 作用域插槽

# 员工列表页面

目标:实现员工列表页面的基本布局和结构

组件文件路径 src/employees/index.vue

<template>
  <div class="employees-container">
    <div class="app-container">
      <page-tools>
        <template #left>
          <span>总记录数: 16 条</span>
        </template>

        <template #right>
          <el-button type="warning" size="small">excel导入</el-button>
          <el-button type="danger" size="small">excel导出</el-button>
          <el-button type="primary" size="small">新增员工</el-button>
        </template>
      </page-tools>

      <el-card style="margin-top: 10px;">
        <el-table border>
          <el-table-column label="序号" sortable="" />
          <el-table-column label="姓名" sortable="" />
          <el-table-column label="工号" sortable="" />
          <el-table-column label="聘用形式" sortable="" />
          <el-table-column label="部门" sortable="" />
          <el-table-column label="入职时间" sortable="" />
          <el-table-column label="账户状态" sortable="" />
          <el-table-column label="操作" sortable="" fixed="right" width="280">
            <template>
              <el-button type="text" size="small">查看</el-button>
              <el-button type="text" size="small">转正</el-button>
              <el-button type="text" size="small">调岗</el-button>
              <el-button type="text" size="small">离职</el-button>
              <el-button type="text" size="small">角色</el-button>
              <el-button type="text" size="small">删除</el-button>
            </template>
          </el-table-column>
        </el-table>
        <!-- 分页组件 -->
        <div style="height: 60px; margin-top: 10px">
          <el-pagination layout="prev, pager, next" />
        </div>
      </el-card>
    </div>
  </div>
</template>

总结:顶部基于封装的PageTools组件实现导航;下面是员工列表布局(表格和分页布局)

# 获取员工列表数据

**目标**实现员工的数据加载渲染

  1. 封装获取员工数据的接口
  2. 获取之后填充表格数据
  • 首先,封装员工的加载请求 src/api/employees.js
// 获取员工列表数据
export function reqGetEmployeeList(options) {
  return request({
    methods: 'get',
    url: '/sys/user',
    params: options
  })
}
  • 然后,实现加载数据和分页的逻辑
import { reqGetEmployeeList } from '@/api/employees'
export default {
  name: 'Employees',
  data () {
    return {
      // 员工列表数据
      list: [],
      // 列表总数
      total: 0,
      // 查询参数
      filterParams: {
        page: 1,
        size: 10
      }
    }
  },
  created () {
    this.loadEmployeeList()
  },
  methods: {
    // 获取员工列表数据
    async loadEmployeeList () {
      try {
        const ret = await reqGetEmployeeList(this.filterParams)
        this.list = ret.data.rows
        this.total = ret.data.total
        if (!ret.success) {
          this.$message.error(ret.message)
        }
      } catch {
        this.$message.error('获取员工列表失败')
      }
    }
  }
}
  • 把数据绑定到表格
<el-card style="margin-top: 10px;">
  <el-table v-loading="loading" border :data="list">
    <el-table-column label="序号" type="index" sortable="" />
    <el-table-column label="姓名" prop="username" sortable="" />
    <el-table-column label="工号" prop="workNumber" sortable="" />
    <el-table-column label="聘用形式" prop="formOfEmployment" sortable="" />
    <el-table-column label="部门" prop="departmentName" sortable="" />
    <el-table-column label="入职时间" prop="timeOfEntry" sortable="" />
    <el-table-column label="账户状态" prop="enableState" sortable="" />
    <el-table-column label="操作" sortable="" fixed="right" width="280">
      <template>
        <el-button type="text" size="small">查看</el-button>
        <el-button type="text" size="small">转正</el-button>
        <el-button type="text" size="small">调岗</el-button>
        <el-button type="text" size="small">离职</el-button>
        <el-button type="text" size="small">角色</el-button>
        <el-button type="text" size="small">删除</el-button>
      </template>
    </el-table-column>
  </el-table>
  <!-- 分页组件 -->
  <div style="height: 60px; margin-top: 10px">
    <el-pagination layout="prev, pager, next" />
  </div>
</el-card>

总结:调用接口;获取数据;填充页面

# 员工列表的分页处理

目标: 处理分页数据

  • 分页结构
<div style="height: 60px; margin-top: 10px">
  <el-pagination
    :total="total"
    :current-page="page"
    :page-size="size"
    layout="prev, pager, next"
    @current-change="handleCurrentChange"
  />
</div>
  • 代码完善
handleCurrentChange(index) {
  this.filterParams.page = index
  this.loadEmployeeList()
}

# 员工列表数据格式化

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

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

# 列格式化-处理聘用形式

该数据的存放文件位于我们提供的**资源/枚举中,可以将枚举下的文件夹放于src/api**文件夹下 @/api/constant/employees.js文件

1 表示正式;2 表示非正式

  • 聘用形式数据 @/api/constant/employees.js
// 聘用形式
hireType: [{
  id: 1,
  value: '正式'
}, {
  id: 2,
  value: '非正式'
}],
  • 基于列的formatter定制列表数据
<el-table-column label="聘用形式" prop="formOfEmployment" sortable="" :formatter='formatHireType'/>
import Types from '@/api/constant/employees'
// 格式化聘用形式
formatHireType (row) {
  // find的返回值是其中一项数据
  const obj = Types.hireType.find(item => {
    return item.id === row.formOfEmployment
  })
  return obj.value
},
  • 基于过滤器方式实现
<el-table-column label="聘用形式" prop="formOfEmployment" sortable="">
  <template v-slot='scope'>
    {{scope.row.formOfEmployment|formatType}}
  </template>
</el-table-column>
filters: {
  // 定义过滤器格式化聘用形式
  formatType (value) {
    // find的返回值是其中一项数据
    const obj = Types.hireType.find(item => {
      return item.id === value
    })
    return obj.value
  }
},

总结:

  1. 基于formatter属性实现,这是由ElementUI提供的规则
  2. 基于Vue的过滤器实现(作用域插槽)

# 过滤器-处理时间格式

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

<el-table-column label="入职时间" prop="timeOfEntry" sortable="">
  <template #default="{ row }">
    {{ row.timeOfEntry | formatTime }}
  </template>
</el-table-column>
  • 通过插件机制扩展过滤器
/*
  封装Vue插件
*/
import PageTools from '@/components/PageTools/index.vue'
import moment from 'moment'

export default {
  install(Vue, options) {
    // 扩展一个自定义指令,处理图片加载失败的情况
    // <img v-imgerror='default.png' src="a.png" alt=""/>
    Vue.directive('imgerror', {
      // bindings包含指令相关的参数信息
      inserted(el, bindings) {
        // console.dir(bindings)
        // 如何知道img标签图片加载失败了?
        el.onerror = () => {
          // 加载失败后触发该函数
          el.src = bindings.value || options.defaultImg
        }
      }
    })

    // 扩展全局组件
    Vue.component('page-tools', PageTools)

    // 扩展过滤器
    Vue.filter('formatTime', (value) => {
      return moment(value).format('yyyy-MM-DD')
    })
  }
}

总结:

  1. 过滤器的定义方式
  2. 插件的用法
  3. 日期处理第三方包moment用法

# 账户状态-switch开关

账户状态,可以用开关组件switch进行显示, 这里用 :value, 将来发送请求, 请求成功了再更新状态

<el-table-column label="账户状态" prop="enableState" sortable="">
  <template #default="{ row }">
    <el-switch
      :value="row.enableState === 1"
      active-color="#13ce66"
      inactive-color="#ff4949"
    />
  </template>
</el-table-column>

总结:

  1. 作用域插槽
  2. el-switch组件的基本使用

# 删除员工

**目标**实现删除员工的功能 (接口文档中没有体现, 但是实际有这个接口)

  1. 封装接口
  2. 绑定事件
  3. 调用接口删除
  4. 刷新列表
  • 首先封装 删除员工的请求
export function reqDelEmployee(id) {
  return request({
    method: 'delete',
    url: `/sys/user/${id}`
  })
}
  • 删除事件绑定
<el-table-column label="操作" sortable="" fixed="right" width="280">
  <template #default="{ row }">
    <el-button type="text" size="small">查看</el-button>
    <el-button type="text" size="small">转正</el-button>
    <el-button type="text" size="small">调岗</el-button>
    <el-button type="text" size="small">离职</el-button>
    <el-button type="text" size="small">角色</el-button>
    <el-button type="text" size="small" @click="handleDelete(row.id)">删除</el-button>
  </template>
</el-table-column>
// 删除员工
handleDelete (id) {
  // 删除部门
  this.$confirm('确认要删除员工吗?, 是否继续?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(async () => {
    // 确认删除
    const ret = await reqDelEmployee(id)
    if (!ret.success) {
      this.$message.error(ret.message)
    } else {
      // 刷新列表
      if (this.total % this.filterParams.size === 1 && this.filterParams.page > 1) {
        this.filterParams.page -= 1
      }
      this.loadEmployeeList()
    }
  }).catch((e) => {
    if (e !== 'cancel') {
      this.$message.error('删除员工失败')
    }
  })
},

总结:封装接口;绑定事件;调用接口删除;刷新列表

# 新增员工

# 新建员工弹层组件

类似**组织架构**的组件,同样新建一个弹层组件 src/views/employees/components/add-employee.vue

<template>
  <el-dialog title="新增员工" :visible="showDialog">
    <!-- 表单 -->
    <el-form label-width="120px">
      <el-form-item label="姓名">
        <el-input style="width:50%" placeholder="请输入姓名" />
      </el-form-item>
      <el-form-item label="手机">
        <el-input style="width:50%" placeholder="请输入手机号" />
      </el-form-item>
      <el-form-item label="入职时间">
        <el-date-picker style="width:50%" placeholder="请选择入职时间" />
      </el-form-item>
      <el-form-item label="聘用形式">
        <el-select style="width:50%" placeholder="请选择" />
      </el-form-item>
      <el-form-item label="工号">
        <el-input style="width:50%" placeholder="请输入工号" />
      </el-form-item>
      <el-form-item label="部门">
        <el-input style="width:50%" placeholder="请选择部门" />
      </el-form-item>
      <el-form-item label="转正时间">
        <el-date-picker style="width:50%" placeholder="请选择转正时间" />
      </el-form-item>
    </el-form>
    <!-- footer插槽 -->
    <template v-slot:footer>
      <el-button>取消</el-button>
      <el-button type="primary">确定</el-button>
    </template>
  </el-dialog>
</template>

<script>
export default {
  props: {
    showDialog: {
      type: Boolean,
      default: false
    }
  }
}
</script>

<style>

</style>

# 控制弹窗的显示

  • 父组件中引用,弹出层
import AddEmployee from './components/add-employee'
components: {
  AddEmployee
},
<add-employee :show-dialog.sync="showDialog" />
  • 点击按钮, 显示弹层
<el-button icon="plus" type="primary" size="small" @click="showDialog = true">新增员工</el-button>
  • top 属性, 配置弹层位置(top是dialog组件的属性,值vh是相对单位)
<el-dialog title="新增员工" :visible="showDialog" top="8vh">

# 添加 dialog 的关闭功能

  • 给父组件传递的prop时, 加上 .sync 修饰符
<add-employee :show-dialog.sync="showDialog" />
  • 注册事件, 提供方法, 关闭弹层
<el-dialog title="新增员工" :visible="showDialog" top="8vh" @close="handleClose">
  
<el-button @click="handleClose">取消</el-button>
closeDialog() {
  this.$emit('update:showDialog', false)
}

总结:控制弹窗的显示和隐藏

  1. 父组件向子组件传值
  2. 子组件向父组件传值
  3. 父和子之间传值的简化写法 sync修饰符用法

add-employee.vue准备数据

data() {
  return {
    formData: {
      username: '', // 用户名
      mobile: '', // 手机号
      formOfEmployment: '', // 聘用形式
      workNumber: '', // 工号
      departmentName: '', // 部门
      timeOfEntry: '', // 入职时间
      correctionTime: '' // 转正时间
    }
  }
},
  • 绑定数据, 绑定校验
<el-form ref="addForm" :model="formData" :rules="rules" label-width="120px">
  <el-form-item label="姓名" prop="username">
    <el-input v-model="formData.username" style="width:50%" placeholder="请输入姓名" />
  </el-form-item>
  <el-form-item label="手机" prop="mobile">
    <el-input v-model="formData.mobile" style="width:50%" placeholder="请输入手机号" />
  </el-form-item>
  <el-form-item label="入职时间" prop="timeOfEntry">
    <el-date-picker v-model="formData.timeOfEntry" style="width:50%" placeholder="请选择入职时间" />
  </el-form-item>
  <el-form-item label="聘用形式" prop="formOfEmployment">
    <el-select v-model="formData.formOfEmployment" style="width:50%" placeholder="请选择" />
  </el-form-item>
  <el-form-item label="工号" prop="workNumber">
    <el-input v-model="formData.workNumber" style="width:50%" placeholder="请输入工号" />
  </el-form-item>
  <el-form-item label="部门" prop="departmentName">
    <el-input v-model="formData.departmentName" style="width:50%" placeholder="请选择部门" />
  </el-form-item>
  <el-form-item label="转正时间" prop="correctionTime">
    <el-date-picker v-model="formData.correctionTime" style="width:50%" placeholder="请选择转正时间" />
  </el-form-item>
</el-form>
  • 指定规则
rules: {
  username: [
    { required: true, message: '用户姓名不能为空', trigger: ['blur', 'change'] },
    { min: 1, max: 4, message: '用户姓名为1-4位', trigger: ['blur', 'change'] }
  ],
  mobile: [
    { required: true, message: '手机号不能为空', trigger: ['blur', 'change'] },
    { pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: ['blur', 'change'] }
  ],
  formOfEmployment: [
    { required: true, message: '聘用形式不能为空', trigger: ['blur', 'change'] }
  ],
  workNumber: [
    { required: true, message: '工号不能为空', trigger: ['blur', 'change'] }
  ],
  departmentName: [
    { required: true, message: '部门不能为空', trigger: ['blur', 'change'] }
  ],
  timeOfEntry: [
    { required: true, message: '请选择入职时间', trigger: ['blur', 'change'] }
  ]
}

总结:表单验证基本配置

# 聘用形式数据填充

  • 导入常量数据
import Types from '@/api/constant/employees.js'
  • 配置data数据
hireType: Types.hireType,
  • 模板中使用数据
<el-form-item label="聘用形式" prop="formOfEmployment">
  <el-select v-model="formData.formOfEmployment" style="width:50%" placeholder="请选择">
    <el-option
      v-for="item in hireType"
      :key="item.id"
      :label="item.value"
      :value="item.id">
    </el-option>
  </el-select>
</el-form-item>

# 部门数据转化树形格式

员工的部门是从树形部门中选择一个部门

<el-form-item label="部门" prop="departmentName">
  <el-input @focus="getDepartments" v-model="formData.departmentName" style="width:50%" placeholder="请选择部门" />
</el-form-item>
import { reqGetDepartments } from '@/api/departments'

data() {
  return {
    ...,
    depts: [], // 定义数组接收树形数据
  }
},
methods: {
    // 把列表数据转换为树形结构
    translateListToTreeData (list, id) {
      // 遍历每一个元素,并且判断每一项pid是否可以参数的pid一致,这样可以找到参数pid所属的下属部门
      const arr = []
      list.forEach(item => {
        if (item.pid === id) {
          // 如果当前部门和父级部门匹配,再去找当前部门的下级部门
          // children就是item的下级所有部门
          const children = this.translateListToTreeData(list, item.id)
          if (children.length > 0) {
            // 证明找到了当前部门的子部门
            item.children = children
          }
          arr.push(item)
        }
      })
      return arr
    },
    async getDepartments () {
      // 这里获取的原始部门数据是数组
      const ret = await reqGetDepartments()
      // 把列表数据转换为树形数据
      this.depts = this.translateListToTreeData(ret.data.depts, '')
      // 获取数据后显示tree组件
      this.showTree = true
    },
    closeDialog() {
      this.$emit('update:showDialog', false)
    },
}

总结:部门输入框获取焦点时,触发接口调用,获取部门列表数据并转换为树形结构

# 结合 el-tree 组件展示

  • 准备tree结构
<el-form-item label="部门" prop="departmentName">
  <el-input @focus="getDepartments" v-model="formData.departmentName" style="width:50%" placeholder="请选择部门" />
  <el-tree
      v-if="showTree"
      v-loading="loading"
      :data="depts"
      :props="{ label: 'name' }"
    />
</el-form-item>
  • 控制显示隐藏
showTree: false, // 是否显示tree

async getDepartments() {
  this.showTree = true
  const { data } = await reqGetDepartments()
  this.treeData = tranListToTreeData(data.depts, '')
}

总结:通过状态位showTree控制树形结构的显示和隐藏

# 点击选择部门表单数据填充

  • 注册 node-click 事件
<el-tree
  v-if="showTree"
  v-loading="loading"
  :data="treeData"
  :props="{ label: 'name' }"
  @node-click="selectNode"
/>
  • 添加事件处理
// 选中树形部门其中一个节点
selectNode (dept) {
  // 控制仅仅可以选择叶子节点(最下面一层部门)
  if (dept.children && dept.children.length > 0) return
  // 获取部门的名称
  this.formData.departmentName = dept.name
  // 隐藏树形结构
  this.showTree = false
},
  • 优化样式:
<div v-if="showTree" class="tree-box">
  <el-tree
    v-loading="loading"
    :data="treeData"
    :props="{ label: 'name' }"
    @node-click="selectNode"
  />
</div>
<style lang="scss" scoped>
.tree-box {
  position: absolute;
  width: 50%;
  min-height: 50px;
  left: 0;
  top: 45px;
  z-index: 100;
  border: 1px solid #ccc;
  border-radius: 4px;
  padding-right: 5px;
  overflow: hidden;
  background-color: #fff;
}
</style>

总结:

  1. 选中部门名称
  2. 美化样式

# 完成新增功能

添加流程:(注意:添加的聘用形式是字符串类型的)

  1. 封装一个接口提交表单
  2. 调用接口提交表单
  3. 关闭弹窗并刷新列表
  • 封装新增员工**api **src/api/employees.js
export function reqAddEmployee(data) {
  return request({
    method: 'post',
    url: '/sys/user',
    data
  })
}
  • 注册点击事件
<el-button type="primary" @click="handleSubmit">确定</el-button>
import { reqAddEmployee } from '@/api/employees'
// 提交表单
handleSubmit () {
  this.$refs.formData.validate(async valid => {
    if (!valid) return
    // 调用新增接口
    const ret = await reqAddEmployee(this.formData)
    if (!ret.success) {
      this.$message.error(ret.message)
    }
    this.$emit('update:showDialog', false)
    // 通知到父组件更新数据
    this.$parent.loadEmployeeList()
  })
},
  • 重置数据(点击取消按钮)
// 关闭弹窗
handleClose () {
  // 关闭弹窗
  this.$emit('update:showDialog', false)
  // 重置表单(只能重置需要验证的输入域)
  this.$refs.formData.resetFields() // 保证验证提示默认不显示
  this.formData = this.$options.data().formData
}

总结:this.$parent代表当前组件的父组件,通过它可以访问父组件的实例方法

注意:重置表单的简化写法:this.formData = this.$options.data().formData

this.formData = this.$options.data() 可以获取原始的表单数据

# 回顾

  • 角色管理
    • 删除角色
    • 控制弹窗的显示和隐藏
    • 添加角色
    • 编辑角色
    • 公司信息的动态渲染
  • 员工管理
    • 熟悉员工管理的基本业务流程
    • 封装顶部的导航组件(具名插槽)
    • 员工列表的动态渲染
    • 定制列表的列效果(作用域插槽)
      • 聘用形式
      • 入职日期
      • 用户的状态
    • 删除员工
    • 添加员工
      • 控制弹窗的显示与隐藏
      • 表单验证
      • 聘用形式的数据填充(和列表展示的聘用形式对应的)
      • 获取部门的名称
        • 点击部门输入域,需要显示部门树形列表
        • 动态获取部门数据并转成树形结构然后展示
        • 点中其中一个部门后,获取对应的部门名称,隐藏树形列表

# 员工导入

功能描述:

刚才我们完成的员工添加是一个一个进行的,实际情况中有时候需要我们一次性添加多个员工信息,这个时候就需要我们开发一个批量导入的功能,点击excel导入按钮,选择准备好要导入的excel表格文件,进行批量添加

image-20210331111553270

image-20210414092137509

# 基本导入组件封装

目标:封装一个导入excel数据 的组件

vue-element-admin已经提供了上传Excel文件的组件,我们只需要改造即可 代码地址 (opens new window)

excel导入功能需要使用npm包**xlsx,所以需要安装xlsx**插件 npm i xlsx

基于vue-element-admin提供的导入功能新建一个组件,位置: src/components/UploadExcel/index.vue

  • 点击导入进入如下的页面

image-20210414094151435

  • 修改样式和布局
<template>
 <div class="upload-excel">
    <div class="btn-upload">
      <el-button :loading="loading" size="mini" type="primary" @click="handleUpload">
        点击上传
      </el-button>
    </div>

    <input ref="excel-upload-input" class="excel-upload-input" type="file" accept=".xlsx, .xls" @change="handleClick">
    <div class="drop" @drop="handleDrop" @dragover="handleDragover" @dragenter="handleDragover">
      <i class="el-icon-upload" />
      <span>将文件拖到此处</span>
    </div>
  </div>
</template>

<style scoped lang="scss">
.upload-excel {
  display: flex;
  justify-content: center;
  margin-top: 100px;
  .excel-upload-input {
    display: none;
    z-index: -9999;
  }
  .btn-upload,
  .drop {
    border: 1px dashed #bbb;
    width: 350px;
    height: 160px;
    text-align: center;
    line-height: 160px;
  }
  .drop {
    padding-top: 20px;
    line-height: 80px;
    color: #bbb;
    i {
      font-size: 60px;
      display: block;
    }
  }
}
</style>
  • 注册全局的导入excel组件 @/components/index.js
// 导入组件
import UploadExcel from './UploadExcel'

export default {
  install(Vue) {
    // 进行组件的全局注册
    Vue.component('UploadExcel', UploadExcel) // 注册导入excel组件
  }
}

总结:基于vue-element-admin提供的导入Excel的案例封装一个组件并且配置全局插件

# 建立公共导入的页面路由

创建路由组件实现导入效果

  1. 创建路由组件
  2. 配置路由
  3. 点击导入按钮实现编程式导航跳转
  • 新建一个公共的导入页面, 即import路由组件 src/views/import/index.vue
<template>
  <upload-excel />
</template>

<script>
export default {
  name: 'Import'
}
    
</script>

<style>

</style>
  • 挂载路由**src/router/index.js**
{
    path: '/import',
    component: Layout,
    hidden: true, // 隐藏在左侧菜单中
    children: [{
      path: '', // 二级路由path什么都不写 表示二级默认路由
      component: () => import('@/views/import')
    }]
},
  • 分析excel导入代码

从点击按钮开始, 分析代码, 这里点击按钮, 触发了input:file 的click事件, 进行了上传

<template #right>
  <el-button @click="$router.push('/import')" type="warning" size="small">excel导入</el-button>
  <el-button type="danger" size="small">excel导出</el-button>
  <el-button type="primary" size="small" @click="handleAdd">新增员工</el-button>
</template>
  • 复制vue-element-admin官方实例script代码到 src/components/UploadExcel/index.vue文件中
<template>
  <upload-excel :on-success='handleSuccess' />
</template>

注意:onSuccess函数会在选中文件后自动触发

export default {
  name: 'Import',
  methods: {
    handleSuccess ({ header, results }) {
      // header是Excel文件的表头(数组)
      // results是Excel文件中的具体数据(数组--里面放的是对象)
      console.log(header, results)
    }
  }
}
  • 分析上传Excel代码结构
    • 点击按钮实现文件数据解析
    • 拖拽实现文件数据解析
<!-- drop 放开鼠标时触发 -->
<!-- dragover 拖拽到目标区域后一直触发 -->
<!-- dragenter 进入目标区域触发一次 -->

# 实现excel导入功能

  • 封装导入员工的api接口
export function reqImportEmployee(data) {
  return request({
    url: '/sys/user/batch',
    method: 'post',
    data
  })
}
  • 根据文件中解析的数据转换为接口需要的数据格式
export default {
  name: 'Import',
  methods: {
    // 转换数据格式
    translateData (results) {
      // 中文和属性的映射关系
      const userRelations = {
        '入职日期': 'timeOfEntry',
        '手机号': 'mobile',
        '姓名': 'username',
        '转正日期': 'correctionTime',
        '工号': 'workNumber'
      }
      // results = [{"手机号":15751786628,"姓名":"张飞1","入职日期":43535,"转正日期":43719,"工号":88088},{"手机号":15751786630,"姓名":"关羽2","入职日期":43535,"转正日期":43719,"工号":88089}]
      const arr = []
      results.forEach(item => {
        let row = {}
        for (let key in item) {
          // 把每一个属性都更换为英文的
          let name = userRelations[key]
          let value = item[key]
          row[name] = value
        }
        arr.push(row)
      })
      return arr
    },
    handleSuccess ({ results }) {
      // header是Excel文件的表头(数组)
      // results是Excel文件中的具体数据(数组--里面放的是对象)
      // 接下来需要把header和results数据转换为接口需要的数据
      /*
        const params = [{
          mobile          
          formOfEmployment
          workNumber      
          departmentName  
          timeOfEntry     
          correctionTime
        }]
      */
      // 需要把中文的key映射为英文的属性名称
      // ["手机号","姓名","入职日期","转正日期","工号"]
      // console.log(JSON.stringify(header))
      // [{"手机号":15751786628,"姓名":"张飞1","入职日期":43535,"转正日期":43719,"工号":88088},{"手机号":15751786630,"姓名":"关羽2","入职日期":43535,"转正日期":43719,"工号":88089}]
      // console.log(JSON.stringify(results))
      // 这里需要返回我们调用接口需要的参数格式即可
      const params = this.translateData(results)
      console.log(params)
    }
  }
}
  • 调用接口实现数据导入
const params = this.translateData(results)
try {
  const ret = await reqImportEmployee(params)
  if (!ret.success) {
    this.$message.error(ret.message)
  } else {
    // 导入成功,退回上一页
    this.$router.back()
  }
} catch {
  this.$message.error('批量导入失败')
}
  • 当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 < 10 ? '0' + month : month) + format + (date < 10 ? '0' + date : date)
  }
  return year + (month < 10 ? '0' + month : month) + (date < 10 ? '0' + date : date)
}

需要注意,导入的手机号不能和之前的存在的手机号重复

  • 处理日期:
// 转换数据格式
translateData (results) {
  // 中文和属性的映射关系
  const userRelations = {
    '入职日期': 'timeOfEntry',
    '手机号': 'mobile',
    '姓名': 'username',
    '转正日期': 'correctionTime',
    '工号': 'workNumber'
  }
  // results = [{"手机号":15751786628,"姓名":"张飞1","入职日期":43535,"转正日期":43719,"工号":88088},{"手机号":15751786630,"姓名":"关羽2","入职日期":43535,"转正日期":43719,"工号":88089}]
  const arr = []
  results.forEach(item => {
    let row = {}
    for (let key in item) {
      // 把每一个属性都更换为英文的
      let name = userRelations[key]
      let value = item[key]
      if (['timeOfEntry', 'correctionTime'].includes(name)) {
        // 这个属性是时间,需要把数字转换为年月日格式
        row[name] = this.formatDate(value)
      } else {
        // 非日期格式不做处理
        row[name] = value
      }
    }
    arr.push(row)
  })
  return arr
},

总结:

  1. 基于现场的案例封装通用的上传导入组件
  2. 准备路由组件实现导入效果
  3. 把选中的Excel文件的内容解析为数据
  4. 分析解析数据的过程(点击选中;拖拽选中)
  5. 把获取的数据转换为接口需要的数据
  6. 调用接口实现导入
  7. 处理导入的日期格式

# 员工导出

image-20210414151409111

# 导出基本演示

导入功能基于vue-element-admin实现,同理,导出功能也是基于它实现(参考export-excel.vue (opens new window)

  1. 把对应文件下载下来(在课程资源/excel导出目录下的 vender,放置到src目录下)
  2. 安装相关的依赖包
npm i xlsx file-saver script-loader
  1. 按需导入模块(通过import()方法导入的文件在项目打包时,不会合并到一块,而是单独打包为一个文件),方便按需进行加载(需要用到该文件的时候再去加载),这样做的好处是提升首屏渲染效率。
import('@/vendor/Export2Excel').then(excel => {
  excel.export_json_to_excel({
    header: ['姓名', '工资'], // 表头 必填
    data: [
      ['刘备', 100],
      ['关羽', 500]
    ], // 具体数据 必填
    filename: 'excel-list', // 非必填
    autoWidth: true, // 非必填
    bookType: 'xlsx' // 非必填
  })
})

# excel导出参数的介绍

  • 参数说明
参数 说明 类型 可选值 默认值
header 导出数据的表头 Array / []
data 导出的具体数据 Array / [[]]
filename 导出文件名 String / excel-list
autoWidth 单元格是否要自适应宽度 Boolean true / false true
bookType 导出文件类型 String xlsx, csv, txt, more (opens new window) xlsx

# excel导出基本的结构

我们最重要的一件事,就是把 表头 和 数据 进行相应的对应,

  • 因为数据中的key是英文,想要导出的表头是中文的话,需要将中文和英文做对应
const headers = {
  '姓名': 'username',
  '手机号': 'mobile',
  '入职日期': 'timeOfEntry',
  '聘用形式': 'formOfEmployment',
  '转正日期': 'correctionTime',
  '工号': 'workNumber',
  '部门': 'departmentName'
}
  • 然后,完成导出代码
handleExport () {
  import('@/vendor/Export2Excel').then(async excel => {
    const headers = {
      '姓名': 'username',
      '手机号': 'mobile',
      '入职日期': 'timeOfEntry',
      '聘用形式': 'formOfEmployment',
      '转正日期': 'correctionTime',
      '工号': 'workNumber',
      '部门': 'departmentName'
    }
    // 获取表头
    const header = Object.keys(headers)
    // 从后端查询所有员工原始数据
    const ret = await reqGetEmployeeList({
      page: 1,
      size: this.total
    })
    // 把原始数据转换为二维数组
    const data = this.jsonToArray(headers, ret.data.rows)
    // 实现导出
    excel.export_json_to_excel({
      header: header,
      data: data, // 具体数据 必填
      filename: 'excel-list', // 非必填
      autoWidth: true, // 非必填
      bookType: 'xlsx' // 非必填
    })
  })
},

jsonToArray(headers, rows) {
  return rows.map(item => {
    return Object.keys(headers).map(key => {
      return item[headers[key]]
    })
  })
}
// 将表头数据和数据进行对应
jsonToArray(headers, rows) {
  const dataArr = rows.map(item => {
    // item是一个对象
    const arr = Object.keys(headers).map(key => {
      return item[headers[key]]
    })
    return arr
  })
  return dataArr
}
// 把json格式数据转换为二维数组
jsonToArray (headers, jsonData) {
  const results = []
  jsonData.forEach(row => {
    // row就是每一行数据
    const person = []
    for (const key in headers) {
      // 获取属性名称
      const attrName = headers[key]
      // 根据属性名称获取对应数据
      const attrValue = row[attrName]
      // 把每一列的数据放到第二维数组中
      person.push(attrValue)
    }
    results.push(person)
  })
  return results
},

总结:

  1. 需要把后端获取的所有员工的数据获取到
  2. 把原始的数据转换为Excel需要的数据(算法)
  3. 基于vendor提供API实现导出

# 导出时间和聘用形式的格式处理

// 把json格式数据转换为二维数组
jsonToArray (headers, jsonData) {
  const results = []
  jsonData.forEach(row => {
    // row就是每一行数据
    const person = []
    for (const key in headers) {
      // 获取属性名称
      const attrName = headers[key]
      // 根据属性名称获取对应数据
      let attrValue = row[attrName]
      // 转换数据格式
      if (['timeOfEntry', 'correctionTime'].includes(attrName)) {
        // 处理时间
        attrValue = moment(attrValue).format('yyyy-MM-DD')
      } else if ('formOfEmployment' === attrName) {
        // 聘用形式
        const obj = Types.hireType.find(item => {
          return item.id === parseInt(attrValue)
        })
        attrValue = obj ? obj.value : ''
      }
      // 把每一列的数据放到第二维数组中
      person.push(attrValue)
    }
    results.push(person)
  })
  return results
},

总结

  1. 时间相关的字段转换为年月日格式(基于moment进行格式化)
  2. 聘用形式需要基于常量数据转换为文字格式