07-HR-01-员工管理模块(通用工具栏的组件结构,员工列表页面,获取员工列表数据,员工列表的分页处理,员工列表数据格式化,员工列表数据格式化,删除员工)

267 阅读8分钟

员工管理模块

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

通用工具栏的组件结构

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

image.png

组件 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) {
    // 扩展全局组件
    Vue.component('page-tools', PageTools)
  }
}

效果图:

image.png

总结:

  1. 封装通用的组件
  2. 插件的用法
  3. 全局组件的用法

具名插槽优化

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

<template>
  <el-card>
    <div class="page-tools">
      <!-- 左侧 -->
      <div class="left">
        <div class="tips" v-if='$slots.fefault'>
            <i class="el-icon-info" />
            <slot/>
        </div>
        <!-- $slots表示父组件传递过来的所有的插槽信息 -->
        <!-- <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>
        <span>本月: 社保在缴 公积金在缴</span>
        <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',
    data: 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()
}

总结:

  1. Elemen分页组件基本用法(相关属性的作用)
  2. 基于页码和每页条数实现分页

员工列表数据格式化

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

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

列格式化-处理聘用形式

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

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

  • 聘用形式数据 @/api/constant/employees.js
// 聘用形式
hireType: [{
  id: 1,
  value: '正式'
}, {
  id: 2,
  value: '非正式'
}],
  • 方法一:基于过滤器方式实现聘用形式的格式化
  // 局部过滤器
  filters: {
    // 过滤器名称
    formatType (value) {
      // 形参表示被过滤器处理的原始数据
      const info = Types.hireType.find(item => item.id === value)
      return info ? info.value : ''
    }
  },
<el-table-column label="聘用形式" prop="formOfEmployment" sortable="">
  <template v-slot="{row}">
    {{ row.formOfEmployment | formatType }}
  </template>
</el-table-column>

总结:过滤器需要先定义,再使用

注意:掌握作用域插槽用法

  • 方法二:基于作用域插槽和方法实现
<el-table-column label="聘用形式" prop="formOfEmployment" sortable="">
    <template v-slot="scope">
     {{ formatHireType(scope.row.formOfEmployment) }}
    </template>
</el-table-column>
formatHireType (type) {
    // 格式化聘用形式
    const obj = Types.hireType.find(item => {
        return item.id === type
    })
    return obj ? obj.value : '---'
},
  • 方法三:基于列的formatter定制列表数据:基于Element表格组件
<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 ? obj.value : ''
},

总结:在渲染数据之前,先进行格式处理,再渲染

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

过滤器-处理时间格式

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

<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')
    })
  }
}
<el-table-column label="入职时间" prop="timeOfEntry" sortable="">
    <template v-slot="scope">
     {{ scope.row.timeOfEntry|formatDate }}
    </template>
</el-table-column>

总结:

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

账户状态-switch开关

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

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

总结:

  1. 作用域插槽
  2. el-switch组件的基本使用
  3. 插槽的绑定方式
    1. v-slot='scope'
    2. v-slot:default='scope'
    3. #default='scope'
    4. #default='{row}'

删除员工

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

  1. 封装接口
  2. 绑定事件
  3. 添加提示
  4. 调用接口删除
  5. 刷新列表
  • 首先封装 删除员工的请求
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.code === 10000) {
      // 删除成功,刷新列表
      this.loadEmployeeList()
    } else {
      this.$message.error(ret.message)
    }
  }).catch((err) => {
    if (err !== 'cancel') {
      this.$message.error('删除员工失败!')
    }
  })
},

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

新增员工

新建员工弹层组件

类似**组织架构**的组件,同样新建一个弹层组件 src/views/employees/components/AddEmployee.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/AddEmployee'
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修饰符用法

表单验证

AddEmployee.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'] }
  ]
}

总结:表单验证基本配置

  1. 下拉选项(聘用形式)
  2. 树形组件(所属部门)

聘用形式表单填充

  • 导入常量数据
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="getDeptList" v-model="formData.departmentName" style="width:50%" placeholder="请选择部门" />
</el-form-item>
import { reqGetDepartments } from '@/api/departments'

data() {
  return {
    ...,
    departs: [], // 定义数组接收树形数据
  }
},
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 getDeptList () {
      // 这里获取的原始部门数据是数组
      const ret = await reqGetDepartments()
      // 把列表数据转换为树形数据
      this.departs = this.translateListToTreeData(ret.data.depts, '')
    },
    closeDialog() {
      this.$emit('update:showDialog', false)
    },
}

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

结合 el-tree 组件展示

  • 准备tree结构
<el-form-item label="部门" prop="departmentName">
  <el-input @focus="getDeptList" 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 getDeptList () {
  // 调用接口获取组织架构的数据
  try {
    // 显示树形列表
    this.showTree = true
    this.loading = true
    // 启用加载状态
    const ret = await reqGetDepartments()
    this.departs = this.translateData(ret.data.depts, '')
    this.loading = false
  } catch {
    // 测试代码
    this.$message.error('获取组织架构数据失败')
  }
},

总结:

  1. 通过状态位showTree控制树形结构的显示和隐藏
  2. 控制树的加载状态

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

  • 注册 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
},
  • 优化样式:
<el-tree v-if="showTree" v-loading="loading" class="tree-box" :data="departs" :props="{ label: 'name' }" @node-click="selectNode" />
<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.addForm.validate(async valid => {
    if (!valid) return
    // 添加部门提交表单
    try {
      const ret = await reqAddEmployee(this.formData)
      if (ret.code === 10000) {
        // 添加成功,刷新列表(通知父组件刷新列表)
        // this.$emit('on-success')
        // this.$parent表示父组件实例对象
        // 刷新列表(调用父组件方法)
        this.$parent.loadEmployeeList()
        // 关闭弹窗
        this.$emit('update:showDialog', false)
      } else {
        this.$message.error(ret.message)
      }
    } catch {
      this.$message.error('添加员工失败!')
    }
  })
},
  • 重置数据(点击取消按钮)
// 关闭弹窗
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.png

基本导入组件封装

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

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

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

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

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

image.png

  • 修改样式和布局
<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='onSuccess' />
</template>

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

export default {
  name: 'Import',
  methods: {
    // 导入Excel数据成功回调函数
    onSuccess (data) {
      // data表示读取到的Excel中的最终数据
      console.log(data.header, data.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)
    }
  }
}
  • 调用接口实现数据导入
async onSuccess (data) {
  // 参数data表示解析Excel之后的数据
  // console.log(data.header, data.results)
  const list = this.translateData(data.results)
  if (list && list.length > 0) {
    try {
      const ret = await reqImportEmployee(list)
      if (ret.code === 10000) {
        // 批量导入成功
        this.$router.push('/employees')
      }
    } 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.png

导出基本演示

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

  1. 把对应文件下载下来(在课程资源/excel导出目录下的 vendor,放置到src目录下)
  2. 安装相关的依赖包
npm i 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' // 非必填
  })
})

总结:基于veu-element-admin提供案例实现导出

excel导出参数的介绍

  • 参数说明
参数说明类型可选值默认值
header导出数据的表头Array/[]
data导出的具体数据(二维数组)Array/[[]]
filename导出文件名String/excel-list
autoWidth单元格是否要自适应宽度Booleantrue / falsetrue
bookType导出文件类型Stringxlsx, csv, txt, morexlsx

excel导出基本的结构

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

  • 因为数据中的key是英文,想要导出的表头是中文的话,需要将中文和英文做对应
const headers = {
  '姓名': 'username',
  '手机号': 'mobile',
  '入职日期': 'timeOfEntry',
  '聘用形式': 'formOfEmployment',
  '转正日期': 'correctionTime',
  '工号': 'workNumber',
  '部门': 'departmentName'
}
  • 然后,完成导出代码
// 获取要导出的数据格式:二维数组
getExportData (headers, rawData) {
  const result = []
  rawData.forEach(item => {
    // 产生一行数据
    const row = []
    for (const key in headers) {
      // 获取英文的key
      const enKey = headers[key]
      // 根据英文的key获取对应的值
      const attrValue = item[enKey]
      // 放入一个列的值
      row.push(attrValue)
    }
    result.push(row)
  })
  return result
},
// 导出Excel
async handleExport () {
  // 导出的数据列表表头
  const headers = {
    '姓名': 'username',
    '手机号': 'mobile',
    '入职日期': 'timeOfEntry',
    '聘用形式': 'formOfEmployment',
    '转正日期': 'correctionTime',
    '工号': 'workNumber',
    '部门': 'departmentName'
  }
  // 获取需要导出的原始数据
  const rawData = await reqGetEmployeeList({
    page: 1,
    size: 50
  })
  // 把原始数据处理成导出的数据格式
  const list = this.getExportData(headers, rawData.data.rows)
  // 导出的表格数据的表头
  // const title = ['姓名', '手机号', '入职日期]
  // Object.keys 是ES6提供的函数用于获取对象的所有的key,最终形成数组
  const title = Object.keys(headers)
  // 需要用到的时候才去加载这个js文件
  import('@/vendor/Export2Excel').then(excel => {
    excel.export_json_to_excel({
      header: title, // 表头 必填
      data: list, // 具体数据 必填
      filename: 'excel-list', // 非必填
      autoWidth: true, // 非必填
      bookType: 'xlsx' // 非必填
    })
  })
},

总结:

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

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

// 获取要导出的数据格式:二维数组
getExportData (headers, rawData) {
  const result = []
  rawData.forEach(item => {
    // 产生一行数据
    const row = []
    for (const key in headers) {
      // 获取英文的key
      const enKey = headers[key]
      // 根据英文的key获取对应的值
      let attrValue = item[enKey]
      console.log(attrValue)
      if (['timeOfEntry', 'correctionTime'].includes(enKey)) {
        try {
          attrValue = moment(item[enKey]).format('yyyy-MM-DD')
        } catch {
          attrValue = '无效日期'
        }
      } else if (enKey === 'formOfEmployment') {
        // 聘用形式,转换为汉字
        const info = Types.hireType.find(item => item.id === attrValue)
        attrValue = info ? info.value : ''
      }
      // 放入一个列的值
      row.push(attrValue)
    }
    result.push(row)
  })
  return result
},

总结

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

注意:

  1. 如何保证某些代码出错后,后续代码可以继续?try catch
  2. 不可以假设查询出的结果一定有数据,需要判断 info && info.value