Bootstrap

5.3 iHRM人力资源 - 员工管理 - 新增员工、员工详情

iHRM人力资源 - 员工管理 - 新增员工、员工详情

一、新增员工页面

当我们点击新增按钮的时候,会路由到一个新的页面

image-20240409220116264

我们之前使用的是弹层编辑或者是行内编辑,下面又是一种新的方式

image-20240409215937409

1.1 页面结构

src/views/employee/detail.vue

image-20240409220336515

1.2 路由

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')
  }
  ]
}

image-20240409220800304

可以直接在路径上输入试一下

image-20240409220832981

1.3 跳转到“新增员工”界面

这么跳转

image-20240409221009568

按钮如下所示,便可以跳转

<el-button size="mini" type="primary" @click="$router.push('employee/detail')">添加员工</el-button>

但是出现了下面的问题,是框架的问题,改一下

image-20240409221312298

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: '员工详情',
      icon: 'people'
    }
  }
  ]
}

1.4 数据和校验

image-20240409221800256

具体的规则

image-20240409221809599

<template>
  <div class="dashboard-container">
    <div class="app-container">
      <div class="edit-form">
        <!--表单-->
        <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">
                <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>
      </div>

    </div>
  </div>
</template>

<script>

export default {
  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一个错误
                callback(new Error('转正时间不能小于入职时间'))
                return
              }
            }
            // 如果成功就callback即可
            callback()
          }
        }]
      }

    }
  },
  methods: {
    saveData() {
      this.$refs.userForm.validate()
    }
  }
}
</script>

二、级联组件封装

2.1 实现思路

如下图所示的内容

image-20240410182526885

整体思路如下图所示

image-20240410182743250

级联组件如下图所示

当一个数据集合有清晰的层级结构时,可通过级联选择器逐级查看并选择

image-20240410183825582

image-20240410184752146

image-20240410185002421

image-20240410185028059

2.2 级联组件

2.2.1 创建组件

image-20240410182909312

然后再detail.vue中引入此组件

import SelectTree from '@/views/employee/select-tree.vue'
export default {
  // 组件注册
  components: { SelectTree }
    ...
}    

引入使用

<el-row>
  <el-col :span="12">
    <el-form-item label="部门" prop="departmentId">
      <!-- 放置及联部门组件 会单独封装-->
      <select-tree/>
    </el-form-item>
  </el-col>
</el-row>

效果图

image-20240410183605121

2.2.2 向级联组件赋值数据

工具类的api

/**
 * 列表数据转树形数据
 * rootValue: 其实就是pid(父id)
 */
export function transListToTreeData(list, rootValue) {
  const arr = []
  list.forEach(item => {
    if (item.pid === rootValue) {
      // 找到了匹配的节点
      arr.push(item)
      // 当前节点的id和当前节点的字节点的pid相等
      // 下面的方法其实就是找当前节点的子节点
      const children = transListToTreeData(list, item.id) // 找到的节点的子节点
      item.children = children // 将子节点赋值给当前节点
      // 我们先push再赋值childern也没关系,因为是一个对象,地址是一样的
    }
  })
  return arr
}

级联组件代码

<template>
  <!--防止element-ui的级联组件-->
  <el-cascader size="mini" :options="treeData" :props="props"></el-cascader>
</template>
<script>
import { getDepartment } from '@/api/department'
import { transListToTreeData } from '@/utils'

export default {

  data() {
    return {
      // 赋值给我们的级联组件options属性的
      treeData: [],
      // 声明绑定的对象
      props: {
        // 要展示的字段
        label: 'name',
        // 要存储的字段
        value: 'id'
      }
    }
  },
  created() {
    this.getDepartment()
  },
  methods: {
    async getDepartment() {
      const result = await getDepartment()
      this.treeData = transListToTreeData(result, 0)
    }
  }
}
</script>

效果图

我们发现某个部门下没有数据了,但是选择不上,后面会修复这个bug

因为我们将列表数据转成树形结构的时候,如果一个部门没有子部门的话,我们也给了一个children属性,导致出现了这个情况

改的话,当每个部门没有子部门的时候,就不给children属性了

image-20240410185301335

2.2.3 修改bug

/**
 * 列表数据转树形数据
 * rootValue: 其实就是pid(父id)
 */
export function transListToTreeData(list, rootValue) {
  const arr = []
  list.forEach(item => {
    if (item.pid === rootValue) {
      // 找到了匹配的节点
      arr.push(item)
      // 当前节点的id和当前节点的字节点的pid相等
      // 下面的方法其实就是找当前节点的子节点
      const children = transListToTreeData(list, item.id) // 找到的节点的子节点
      if (children.length) {
        item.children = children // 将子节点赋值给当前节点
      }
      // 我们先push再赋值childern也没关系,因为是一个对象,地址是一样的
    }
  })
  return arr
}

效果图

相当的完美

image-20240410185551635

2.2.4 设置分割符为横线

image-20240410190639274

<template>
  <!--防止element-ui的级联组件-->
  <el-cascader
    size="mini"
    :options="treeData"
    :props="props"
    separator="-"
  ></el-cascader>
</template>

image-20240410190736933

2.2.5 级联组件双向绑定

image-20240410191736677

image-20240410191326745

  1. 父组件修改后,子组件也修改
<el-cascader
  size="mini"
  :options="treeData"
  :props="props"
  separator="-"
  :value="value"
></el-cascader>
props: {
  // v-model接收的属性值必须是value
  // 要把这个属性绑定给级联属性
  value: {
    // 存储的是部门的id
    type: Number,
    default: null
  }
}

在父组件中绑定

image-20240410191845487

<el-col :span="12">
  <el-form-item label="部门" prop="departmentId">
    <!-- 放置及联部门组件 会单独封装-->
    <!--inputW样式会给到select-tree组件中的template第一层的组件-->
    <select-tree class="inputW" v-model="userInfo.departmentId"/>
  </el-form-item>
</el-col>

  data() {
    return {
      userInfo: {
        username: '', // 用户名
        mobile: '', // 手机号
        workNumber: '', // 工号
        formOfEmployment: null, // 聘用形式
        departmentId: null, // 部门id
        timeOfEntry: '', // 入职时间
        correctionTime: '' // 转正时间
      },
   }
  }

当我们把detail的组件的departmentId改成2后,如下图所示,会有一个双向绑定

image-20240410192119712

相当于我们把部门的主键给级联组件后,级联组件就会帮我们找

  1. 子组件修改后,父组件也修改

我们上面完成了一条线,下面再来完成另一条线

当我们选择某个部门后,id发生了变化,那我们应该去触发input的事件

image-20240410192857740

<!--element-ui的级联组件-->
<el-cascader
  size="mini"
  :options="treeData"
  :props="props"
  separator="-"
  :value="value"
  @change="changeValue"
></el-cascader>
    // 参数1:数组
    changeValue(list) {
      // 取到数组的最后一位
      if (list.length > 0) {
        // 将最后一位的id取出,传给了select-tree组件上的v-model属性
        // 而v-model属性又监听了input,所以把id赋值到了userInfo.departmentId
        this.$emit('input', list[list.length - 1])
      } else {
        // 如果没有内容,我们就把值设置为空即可
        this.$emit('input', null)
      }
    }

三、新增员工

image-20240410193736521

流程图如下所示

image-20240410193655213

  1. api请求
/**
 * 新增员工
 */
export function addEmployee(data) {
  return request({
    url: '/sys/user',
    method: 'post',
    data
  })
}
  1. 按钮
<!-- 保存个人信息 -->
<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>
  1. 方法
methods: {
  saveData() {
    this.$refs.userForm.validate(async isOK => {
      if (isOK) {
        // 校验通过
        await addEmployee(this.userInfo)
        this.$message.success('新增成功')
        // 跳转到列表也
        this.$router.push('/employee')
      }
    })
  }
}
  1. 添加完成后的效果图

image-20240410195415048

四、编辑员工

4.1 数据回显

点击查看按钮,跳转到详情页面的时候,要写到这条数据的id(当前点击行数据的id),有了id之后我们才能完成数据的回显

image-20240410200021182

"查看"按钮在这个组件里面

image-20240410200215777

  1. 点击“查看”按钮,跳转到详情页面,并且携带数据id

加了一个路由参数

<!--路由后面加了一个参数,也就是路由的参数-->
<el-button size="mini" type="text" @click="$router.push(`/employee/detail/${row.id}`)">查看</el-button>

修改一下路由

 {
  // 员工详情地址
  // path: '/employee/detail',
  // 这样后就是一个动态的路由参数,此路由参数的字段名是id
  // “?”的意思id这个参数可能有,也可能没有,
  path: '/employee/detail/:id',
  component: () => import('@/views/employee/detail.vue'),
  // 隐藏这个路由
  hidden: true,
  // 这个其实就是一个标题和图标而已
  meta: {
    title: '员工详情',
    icon: 'people'
  }
}

2.在详情页获取路由参数id

image-20240410202201040

  1. api请求
/**
 * 查询员工
 */
export function getEmployeeDetail(id) {
  return request({
    url: `/sys/user/${id}`,
  })
}
  1. 方法
  created() {
    // 如何获取路由参数中的id
    if (this.$route.params.id) {
      // 有id的情况下崽调用
      this.getEmployeeDetail()
    }
  }
methods: {
    async getEmployeeDetail() {
      this.userInfo = await getEmployeeDetail(this.$route.params.id)
    }
    ...
}    
  1. 效果图

image-20240410203408366

4.2 保存

image-20240410203723632

其实就是在新增的基础上,加了一个判断是不是编辑模式,如果是的话,就调用编辑api方法

image-20240410203743041

  1. 按钮
<!-- 保存个人信息 -->
<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>
  1. api
/**
 * 编辑接口
 */
export function updateEmployee(data) {
  return request({
    url: `/sys/user/${data.id}`,
    method: 'post',
    data
  })
}
  1. 方法
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. 修改模式下不让修改手机号
<el-col :span="12">
          <!--修改模式下不让修改手机号,两个叹号,第一个,将值取反成布尔类型的值,第二个,再取回来-->
          <el-form-item label="手机" prop="mobile">
            <el-input
              :disabled="!!$route.params.id"
              v-model="userInfo.mobile"
              size="mini"
              class="inputW"
            />
          </el-form-item>
</el-col>

五、员工上传头像组件

效果及流程图

image-20240410210639038

image-20240410210716543

5.1 封装组件

创建新组件

image-20240410211508212

<template>
  <!--上传的组件-->
  <!--action是上传的地址,但是我们的项目不用,因为action属于自动上传,而我们目前的项目是手动上传-->
  <!--虽然我们不使用action,但是也要留着此属性,否则控制台报错-->
  <!--show-file-list表示展示文件的上传列表,我们目前只上传一个头像而已,不需要列表-->
  <!--before-upload 上传文件之前,此方法主要是为了上传文件前进行检查-->
  <el-upload
    class="avatar-uploader"
    action=""
    :show-file-list="false"
    :before-upload="beforeAvatarUpload"
  >
    <!-- (自动上传)action是上传地址 人资项目不需要 人资项目(手动上传)  -->
    <!-- show-file-list不展示列表 -->
    <img v-if="value" :src="value" class="avatar">
    <i v-else class="el-icon-plus avatar-uploader-icon"/>
  </el-upload>
</template>

<script>
export default {
  props: {
    value: {
      type: String,
      default: ''
    }
  },
  methods: {
    // 检查函数 判断文件的类型还有大小 return  true(继续上传)/false(停止上传)
    beforeAvatarUpload(file) {
      // jpeg png gif bmp

      const isJPG = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp'].includes(file.type)
      // 由byte转换成MB
      const isLt2M = file.size / 1024 / 1024 < 5 // 5M

      if (!isJPG) {
        this.$message.error('上传头像图片只能是 JPG PNG GIF BMP 格式!')
      }
      if (!isLt2M) {
        this.$message.error('上传头像图片大小不能超过 5MB!')
      }
      return isJPG && isLt2M
    }
  }
}
</script>

<style>
.avatar-uploader .el-upload {
  border: 1px dashed #d9d9d9;
  border-radius: 6px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
}

.avatar-uploader .el-upload:hover {
  border-color: #409EFF;
}

.avatar-uploader-icon {
  font-size: 28px;
  color: #8c939d;
  width: 178px;
  height: 178px;
  line-height: 178px;
  text-align: center;
}

.avatar {
  width: 178px;
  height: 178px;
  display: block;
}
</style>

在下面这个组件中应用

image-20240410210716543

<!-- 员工照片 -->
<el-row>
  <el-col :span="12">
    <el-form-item label="员工头像">
      <!-- 放置上传图片 -->
      <image-upload/>
    </el-form-item>
  </el-col>
</el-row>

效果图

image-20240410211646832

5.2 图片回显

  1. 父组件向子组件传输

父组件

image-20240410215752049

<!-- 员工照片 -->
<el-row>
  <el-col :span="12">
    <el-form-item label="员工头像">
      <!-- 放置上传图片 -->
      <image-upload v-model="userInfo.staffPhoto"/>
    </el-form-item>
  </el-col>
</el-row>

子组件 - 员工头像上传组件

image-20240410215806169

<el-upload
  class="avatar-uploader"
  action=""
  :show-file-list="false"
  :before-upload="beforeAvatarUpload"
>
  <!--下面是,如果有图片的话,就显示图片,没有图片的话,就显示一个图标-->
  <!--下面的value值是父组件传输过来的一个值-->
  <img v-if="value" :src="value" class="avatar">
  <i v-else class="el-icon-plus avatar-uploader-icon"/>
</el-upload>
props: {
  // 接收传输过来的value值
  value: {
    type: String,
    default: ''
  }
}

此时的效果图

image-20240410215818880

目前只实现了蓝色的部分,没有实现绿色部分

image-20240410220117318

子组件向父组件传输这部分没有完成

当我们使用组件修改完头像的时候,头像也要形成一个回显操作

5.3 自定义上传图片

要完成定义上传图片功能,我们要上传到腾讯云服务器,我们上传后会给我们返回一个地址,地址执行我们的图片

我们要通过input把这个图片传出去

默认有个action自动上传,但是我们制成了空字符串,因为我们要采用一个自定义上传的方式(手动上传)

需要自己注册一个账号

image-20240410220841811

  1. 增加http-request属性

翻阅文档,有如下Attribute(是一个属性,不是一个事件),我们监听他需要给他传一个函数,不是用@修饰,而是用冒号修饰

image-20240410221259772

如下所示

<el-upload
  class="avatar-uploader"
  action=""
  :show-file-list="false"
  :before-upload="beforeAvatarUpload"
  :http-request="uploadImage"
>
  1. 安装腾讯云上传SDK
npm i cos-js-sdk-v5

image-20240410221855752

import COS from 'cos-js-sdk-v5'
  1. 上传方法
// 选择图片之后上传,上传的内容是params
uploadImage(params) {
  // 完成COS对象的初始化
  const cos = new COS({
    SecretId: 'AKIDDSdjgnjT1NZ3a7VjkfVIwOdfv9IH2b8e',
    SecretKey: 'WEwe9WJ9vLeq1BHNLLKF5Up10ndUDk24'
  })
  // 参数1:对象, 参数2:回调函数,err表示报错信息,data表示要返回的数据
  cos.putObject({
    Bucket: 'heimachengxuyuan-1302806742', // 存储桶名称
    Region: 'ap-nanjing', // 地域名称
    Key: params.file.name, // 文件名称
    StorageClass: 'STANDARD', // 固定值
    Body: params.file // 文件对象
  }, (err, data) => {
    if (data.statusCode === 200 && data.Location) {
      // 拿到了腾讯云返回的地址
      // 通过input自定义事件将地址传出去
      this.$emit('input', 'http://' + data.Location) // 将地址返回了
    } else {
      this.$message.error(err.Message) // 上传失败提示消息
    }
  })
}

image-20240410222557551

image-20240410222412277

方法中的params参数,我们需要里面的file

image-20240410221648137

this.$emit(‘input’, ‘http://’ + data.Location)

正好和父组件对应

image-20240410222929338子组件向父组件传输完成!!!niubi!!!

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;