危险

为之则易,不为则难

0%

07_人资后台

✔ 完成员工的 excel 导入导出/下载功能。

✔ 完成员工的增删改查功能。

员工管理-导出excel

  1. axios 配置 responseType 为 blob,接收的二进制流文件为 Blob 格式,src/api/employee.js
1
2
3
4
5
6
7
8
9
10
/**
* 导出员工的 excel
*/
export function exportEmployee() {
return request({
url: '/sys/user/export',
// 改变接收数据的类型
responseType: 'blob' // 使用 blob 接收二进制文件流
})
}
  1. 响应拦截器判断是不是 blob 类型,如果是直接返回数据,不再进行解构, src/utils/request.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
service.interceptors.response.use(
(response) => {
// mark
if (response.data instanceof Blob) return response.data
const {
success,
data,
message
} = response.data
if (success) {
return data
} else {
Message.error(message)
return Promise.reject(new Error(message))
}
},
async (error) => {
if (error.response.status === 401) {
Message({
type: 'warning',
message: 'token 超时了'
})
await store.dispatch('user/logout')
router.push('/login')
return Promise.reject(error)
}
Message.error(error.message)
return Promise.reject(error)
}
)
  1. 安装 file-saver。
1
npm i file-saver
  1. 点击按钮调用接口,使用 file-saver 将 blob 转化成文件下载, src/views/employee/index.vue
1
<el-button size="mini" @click="exportEmployee">excel导出</el-button>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import FileSaver from 'file-saver'
import {
exportEmployee
} from '@/api/employee'
export default {
name: 'Employee',
// ...
methods: {
// ...
async exportEmployee() {
const result = await exportEmployee()
// FileSaver.saveAs(blob对象, 文件名称)
FileSaver.saveAs(result, '员工信息表.xlsx')
}
}
}

员工管理-excel导入

  1. 创建员工导入组件,src/views/employee/components/import-excel.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<template>
<el-dialog width="500px" title="员工导入" :visible="showExcelDialog" @close="$emit('update:showExcelDialog', false)">
<el-row type="flex" justify="center">
<div class="upload-excel">
<input ref="excel-upload-input" class="excel-upload-input" type="file" accept=".xlsx, .xls">
<div class="drop">
<i class="el-icon-upload" />
<el-button type="text">下载导入模板</el-button>
<span>将文件拖到此处或
<el-button type="text">点击上传</el-button>
</span>
</div>
</div>
</el-row>
<el-row type="flex" justify="end">
<!-- update:props属性名,值 直接修改 .sync修饰符的属性值 -->
<el-button size="mini" type="primary" @click="$emit('update:showExcelDialog', false)">取消</el-button>
</el-row>
</el-dialog>
</template>
<script>
export default {
props: {
showExcelDialog: {
type: Boolean,
default: false
}
},
methods: {

}
}
</script>

<style scoped lang="scss">
.upload-excel {
display: flex;
justify-content: center;
margin: 20px;
width: 360px;
height: 180px;
align-items: center;
color: #697086;

.excel-upload-input {
display: none;
z-index: -9999;
}

.btn-upload,
.drop {
border: 1px dashed #dcdfe6;
width: 100%;
height: 100%;
text-align: center;
line-height: 160px;
border-radius: 8px;
display: flex;
flex-direction: column;
justify-content: center;
}

.drop {
line-height: 40px;
color: #bbb;

i {
font-size: 60px;
display: block;
color: #c0c4cc;
}
}
}
</style>
  1. 在员工管理页面-导入该组件并注册使用,src/views/employee/index.vue
1
2
3
4
5
6
7
import ImportExcel from './components/import-excel.vue'

export default {
components: {
ImportExcel
}
}
  1. 声明一个控制该弹层的变量,src/views/employee/index.vue
1
2
3
4
5
data() {
return {
showExcelDialog: false // 控制excel的弹层显示和隐藏
}
}
  1. 使用该组件,并且应用变量,src/views/employee/index.vue
1
<import-excel :show-excel-dialog.sync="showExcelDialog" />
  1. 点击员工展示导入弹出层,src/views/employee/index.vue
1
<el-button size="mini" @click="showExcelDialog = true">excel导入</el-button>

员工管理-下载导入模板

  1. 封装下载模板的 API,src/api/employee.js
1
2
3
4
5
6
7
8
9
/**
* 下载员工导入模版
*/
export function getExportTemplate() {
return request({
url: '/sys/user/import/template',
responseType: 'blob' // 二进制文件流
})
}
  1. 点击按钮进行下载模板,src/views/employee/components/import-excel.vue
1
<el-button type="text" @click="getTemplate">下载导入模板</el-button>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import {
getExportTemplate
} from '@/api/employee'
import FileSaver from 'file-saver'
export default {
name: 'ImportExcel',
props: {
showExcelDialog: {
type: Boolean,
default: false
}
},
methods: {
async getTemplate() {
const data = await getExportTemplate()
FileSaver.saveAs(data, '员工导入模版.xlsx')
}
}
}

员工管理-员工导入-上传excel

  1. 封装上传 excel 的 API,src/api/employee.js
1
2
3
4
5
6
7
8
9
10
/**
* 上传用户的 excel
*/
export function uploadExcel(data) {
return request({
url: '/sys/user/import',
method: 'post',
data // form-data 类型,因为要上传文件类型
})
}
  1. 点击上传,弹出文件选择框,src/views/employee/components/import-excel.vue
1
<el-button type="text" @click="handleUpload">点击上传</el-button>
1
2
3
handleUpload() {
this.$refs['excel-upload-input'].click() // this.$refs.属性名 和 this.$refs[属性名] 等价
},
  1. 监听文件改变 -> 上传 excel -> 关闭弹层,src/views/employee/components/import-excel.vue
1
<input ref="excel-upload-input" class="excel-upload-input" type="file" accept=".xlsx, .xls" @change="uploadChange">
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
export default {
name: 'ImportExcel',
// ...
methods: {
// ...
async uploadChange(event) {
// 调用上传接口
// uploadExcel() // 参数 form-data 需要文件 file
const files = event.target.files // input 的文件列表
if (files.length > 0) {
// 大于0 说明有文件要上传
const data = new FormData()
// file: file类型
data.append('file', files[0]) // 将文件参数加入到formData中
try {
await uploadExcel(data)
// 成功
this.$emit('uploadSuccess') // 通知父组件 我上传成功
this.$emit('update:showExcelDialog', false) // 关闭弹层
// this.$refs['excel-upload-input'].value = ''
} catch (error) {
// 捕获失败
// this.$refs['excel-upload-input'].value = ''
} finally {
// 不论成功或者失败都会执行finally
this.$refs['excel-upload-input'].value = ''
}
}
}
}
}

这里为什么不管成功或者失败都要清空文件选择器中的内容呢? 因为不论成功或者失败,再点击上传都会去选择一个新的 excel,所以这里使用 finally 等到最后,将内容清空。

父组件需要监听上传成功的事件, src/views/employee/index.vue

1
<import-excel :show-excel-dialog.sync="showExcelDialog" @uploadSuccess="getEmployeeList" />

员工管理-删除员工

  1. 封装删除员工的 API,src/api/employee.js
1
2
3
4
5
6
7
8
9
/**
* 删除员工
*/
export function delEmployee(id) {
return request({
method: 'delete',
url: `/sys/user/${id}`
})
}
  1. 放置气泡框确认框,src/views/employee/index.vue
1
2
3
4
5
<template #default="{row}">
<el-popconfirm title="确认删除该行数据吗?" @onConfirm="confirmDel(row.id)">
<el-button slot="reference" style="margin-left:10px" size="mini" type="text">删除</el-button>
</el-popconfirm>
</template>
  1. 删除方法实现,src/views/employee/index.vue
1
2
3
4
5
6
7
// 删除员工方法
async confirmDel(id) {
await delEmployee(id)
if (this.list.length === 1 && this.queryParams.page > 1) this.queryParams.page--
this.getEmployeeList()
this.$message.success('删除员工成功')
}

添加员工和路由

  1. 创建一个员工详情页面(直接拷贝),src/views/employee/detail.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
<template>
<div class="container">
<div class="app-container">
<el-card>
<div class="edit-form">
<el-form ref="userForm" label-width="220px">
<!-- 姓名 部门 -->
<el-row>
<el-col :span="12">
<el-form-item label="姓名" prop="username">
<el-input size="mini" class="inputW" />
</el-form-item>
</el-col>

</el-row>
<!-- 工号 入职时间 -->
<el-row>
<el-col :span="12">
<el-form-item label="工号" prop="workNumber">
<el-input size="mini" class="inputW" />
</el-form-item>
</el-col>
</el-row>
<!--手机 聘用形式 -->
<el-row>
<el-col :span="12">
<el-form-item label="手机" prop="mobile">
<el-input size="mini" class="inputW" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="部门" prop="departmentId">
<!-- 放置及联部门组件 -->
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="聘用形式" prop="formOfEmployment">
<!-- <el-select size="mini" class="inputW" /> -->
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="入职时间" prop="timeOfEntry">
<el-date-picker size="mini" type="date" value-format="yyyy-MM-dd" class="inputW" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="转正时间">
<el-date-picker size="mini" type="date" class="inputW" />
</el-form-item>
</el-col>
</el-row>
<!-- 员工照片 -->
<el-row>
<el-col :span="12">
<el-form-item label="员工头像">
<!-- 放置上传图片 -->
</el-form-item>
</el-col>
</el-row>
<!-- 保存个人信息 -->
<el-row type="flex">
<el-col :span="12" style="margin-left:220px">
<el-button size="mini" type="primary">保存更新</el-button>
</el-col>
</el-row>
</el-form>
</div>
</el-card>
</div>
</div>
</template>

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

<style scoped lang="scss">
.edit-form {
background: #fff;
padding: 20px;

.inputW {
width: 380px
}
}
</style>
  1. 配置员工详情的路由信息,src/router/modules/employee.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import layout from '@/layout'
export default {
path: '/employee',
component: layout,
children: [{
path: '',
name: 'employee',
component: () => import('@/views/employee'),
meta: {
title: '员工',
icon: 'people'
}
}, {
path: '/employee/detail', // 员工详情的地址
component: () => import('@/views/employee/detail.vue'),
hidden: true, // 表示隐藏在左侧菜单
meta: {
title: '员工详情' // 显示在导航的文本
}
}]
}
  1. 点击添加员工跳转到详情页, src/views/employee/index.vue
1
<el-button size="mini" type="primary" @click="$router.push('/employee/detail')">添加员工</el-button>

添加员工-表单数据校验

表单校验规则如下。

  • 姓名:必填;1~4 个字符。

  • 手机号:必填。

  • 部门:必填。

  • 聘用形式:必填。

  • 入职时间:必填。

  • 转正时间:必填;不能小于入职时间。

  1. 定义数据和规则,src/views/employee/detail.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
export default {
name: 'Employee',
data() {
return {
userInfo: {
username: '', // 用户名
mobile: '', // 手机号
workNumber: '', // 工号
formOfEmployment: null, // 聘用形式
departmentId: null, // 部门 id
timeOfEntry: '', // 入职时间
correctionTime: '' // 转正时间
},
rules: {
username: [{
required: true,
message: '请输入姓名',
trigger: 'blur'
}, {
min: 1,
max: 4,
message: '姓名为1-4位'
}],
mobile: [{
required: true,
message: '请输入手机号',
trigger: 'blur'
}, {
// pattern 正则表达式
pattern: /^1[3-9]\d{9}$/,
message: '手机号格式不正确',
trigger: 'blur'
}],
formOfEmployment: [{
required: true,
message: '请选择聘用形式',
trigger: 'blur'
}],
departmentId: [{
required: true,
message: '请选择部门',
trigger: 'blur'
}],
timeOfEntry: [{
required: true,
message: '请选择入职时间',
trigger: 'blur'
}],
correctionTime: [{
required: true,
message: '请选择转正时间',
trigger: 'blur'
}, {
validator: (rule, value, callback) => {
if (this.userInfo.timeOfEntry) {
if (new Date(this.userInfo.timeOfEntry) > new Date(value)) {
callback(new Error('转正时间不能小于入职时间'))
return
}
}
callback()
}
}]
}
}
},
methods: {
saveData() {
this.$refs.userForm.validate()
}
}
}
  1. 绑定表单数据和属性, src/views/employee/detail.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<el-form ref="userForm" :model="userInfo" :rules="rules" label-width="220px">
<!-- 姓名 -->
<el-row>
<el-col :span="12">
<el-form-item label="姓名" prop="username">
<el-input v-model="userInfo.username" size="mini" class="inputW" />
</el-form-item>
</el-col>
</el-row>
<!-- 工号 -->
<el-row>
<el-col :span="12">
<el-form-item label="工号" prop="workNumber">
<!-- 工号是系统生成的 禁用这个组件-->
<el-input v-model="userInfo.workNumber" disabled size="mini" class="inputW" />
</el-form-item>
</el-col>
</el-row>
<!-- 手机 -->
<el-row>
<el-col :span="12">
<el-form-item label="手机" prop="mobile">
<el-input v-model="userInfo.mobile" size="mini" class="inputW" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="部门" prop="departmentId">
<!-- 放置及联部门组件 会单独封装-->
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="聘用形式" prop="formOfEmployment">
<!-- mark -->
<el-select v-model="userInfo.formOfEmployment" size="mini" class="inputW">
<el-option label="正式" :value="1" />
<el-option label="非正式" :value="2" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="入职时间" prop="timeOfEntry">
<el-date-picker v-model="userInfo.timeOfEntry" size="mini" type="date" value-format="yyyy-MM-dd" class="inputW" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="转正时间" prop="correctionTime">
<el-date-picker v-model="userInfo.correctionTime" size="mini" type="date" class="inputW" />
</el-form-item>
</el-col>
</el-row>
<!-- 员工照片 -->
<el-row>
<el-col :span="12">
<el-form-item label="员工头像">
<!-- 放置上传图片 -->
</el-form-item>
</el-col>
</el-row>
<!-- 保存个人信息 -->
<el-row type="flex">
<el-col :span="12" style="margin-left:220px">
<el-button size="mini" type="primary" @click="saveData">保存更新</el-button>
</el-col>
</el-row>
</el-form>

添加员工-封装部门级联组件

Cascader 级联组件常用的属性如下。

  • options 为一个树形结构的数据源。

  • props 可以设置数据源中的字段如 label(展示),value(存取)。

  • separator 为展示的分隔符。

  1. 创建 select-tree 组件,src/views/employee/components/select-tree.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<template>
<!-- element-ui级联组件 -->
<el-cascader size="mini" :options="treeData" :props="props" separator="-" />
</template>
<script>
import {
getDepartments
} from '@/api/departments'
import {
transListToTreeData
} from '@/utils'
export default {
name: 'SelectTree',
data() {
return {
treeData: [], // 赋值给 级联组件的options
props: {
label: 'name', // 要展示的字段
value: 'id' // 要存储的字段
}
}
},
created() {
this.getDepartments()
},
methods: {
async getDepartments() {
this.treeData = transListToTreeData(await getDepartments(), 0) // 将组织架构的数据 转化树形赋值给treeData
}
}
}
</script>
  1. 使用 select-tree 组件, src/views/employee/detail.vue
1
2
3
4
5
6
7
8
// 引入组件
import SelectTree from './components/select-tree.vue'
export default {
name: 'EmployeeDetail',
components: {
SelectTree
},
}
1
2
3
4
5
<el-form-item label="部门" prop="departmentId">
<!-- 放置级联部门组件会单独封装-->
<!-- inputW 样式会给到 selectTree 中,template 第一层的组件 -->
<select-tree class="inputW" />
</el-form-item>

添加员工-级联组件-双向绑定

  1. 接收 value 属性, src/views/employee/components/select-tree.vue
1
2
3
4
5
6
7
8
export default {
props: {
value: {
type: Number, // 存储的是部门的 id 3 4 5
default: null
}
}
}
  1. 级联改变触发 input 事件, src/views/employee/components/select-tree.vue
1
<el-cascader :value="value" size="mini" :options="treeData" :props="props" separator="-" @change="changeValue" />
1
2
3
4
5
6
7
8
changeValue(list) {
// 取到数组的最后一次
if (list.length > 0) {
this.$emit('input', list[list.length - 1]) // 将最后一位的 id 取出来 传出去
} else {
this.$emit('input', null) // 如果长度为 0 说明值为空
}
}
  1. 在父组件使用 v-model 双向绑定, src/views/employee/detail.vue
1
<select-tree v-model="userInfo.departmentId" class="inputW" />

添加员工-新增员工

  1. 封装新增员工 API, src/api/employee.js
1
2
3
4
5
6
7
export function addEmployee(data) {
return request({
url: '/sys/user',
method: 'post',
data
})
}
  1. 点击保存按钮进行新增, src/views/employee/detail.vue
1
2
3
4
5
6
7
8
9
10
11
12
import { addEmployee } from '@/api/employee'

saveData() {
this.$refs.userForm.validate(async isOK => {
if (isOK) {
// 校验通过
await addEmployee(this.userInfo)
this.$message.success('新增员工成功')
this.$router.push('/employee')
}
})
}

编辑员工-查看员工

  1. 封装获取员工详情的 API, src/api/employee.js
1
2
3
4
5
6
7
8
/**
* 获取员工详情
*/
export function getEmployeeDetail(id) {
return request({
url: `/sys/user/${id}`
})
}
  1. 点击查看时跳转到详情携带 id, src/views/employee/index.vue
1
<el-button size="mini" type="text" @click="$router.push(`/employee/detail/${row.id}`)">查看</el-button>
  1. 配置详情的路由支持新增模式和编辑模式, src/router/modules/employee.js

? 表示可有可无,可以传 id 也可以选择不传,页面都能正确显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export default {
path: '/employee',
component: layout,
children: [{
path: '',
name: 'employee',
component: () => import('@/views/employee'),
meta: {
title: '员工',
icon: 'people'
}
}, {
path: '/employee/detail/:id?', // 员工详情的地址
component: () => import('@/views/employee/detail.vue'),
hidden: true, // 表示隐藏在左侧菜单
meta: {
title: '员工详情' // 显示在导航的文本
}
}]
}
  1. 员工详情页判断是否有 id,有 id 就查询详情数据, src/views/employee/detail.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { getEmployeeDetail } from '@/api/employee'
export default {
name: 'EmployeeDetail',
created() {
// 如何获取路由参数中的 id
this.$route.params.id && this.getEmployeeDetail()
},
// ...
methods: {
// ...
async getEmployeeDetail() {
this.userInfo = await getEmployeeDetail(this.$route.params.id)
}
}
}

编辑员工-保存员工

  1. 封装更新员工的 API, src/api/employee.js
1
2
3
4
5
6
7
export function updateEmployee(data) {
return request({
url: `/sys/user/${data.id}`,
method: 'put',
data
})
}
  1. 保存时区分保存和新增场景, src/views/employee/detail.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { updateEmployee } from '@/api/employee'

saveData() {
this.$refs.userForm.validate(async isOK => {
if (isOK) {
// 编辑模式
if (this.$route.params.id) {
// 编辑模式
await updateEmployee(this.userInfo)
this.$message.success('更新员工成功')
} else {
// 新增模式
// 校验通过
await addEmployee(this.userInfo)
this.$message.success('新增员工成功')
}
this.$router.push('/employee')
}
})
}
  1. 编辑模式下,手机号和工号不可编辑, src/views/employee/detail.vue
1
2
3
4
<!-- 工号 -->
<!-- ... -->
<!-- 手机号 -->
<el-input v-model="userInfo.mobile" :disabled="!!$route.params.id" size="mini" class="inputW" />