# 人力资源中台项目管理
**Repository Path**: fire36/hr
## Basic Information
- **Project Name**: 人力资源中台项目管理
- **Description**: 利用vue2编写开发的人力资源中台项目管理
- **Primary Language**: Unknown
- **License**: MIT
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2022-12-28
- **Last Updated**: 2025-01-04
## Categories & Tags
**Categories**: Uncategorized
**Tags**: vue2, Vuex
## README
# 人资中台后端管理
## 项目启动前的配置工作
### vue.config.js
> ```process.env```环境变量
内容详解
```js
const path = require('path')
const defaultSettings = require('./src/settings.js')
function resolve(dir) {
return path.join(__dirname, dir)
}
const port = process.env.port || process.env.npm_config_port || 9528 // dev port
// process.env.npm_config_port取的是在根目录下的 .npmrc 文件中的属性
const name = defaultSettings.title || 'vue Admin Template' // page title
module.exports = {
publicPath: '/',
outputDir: 'dist',
assetsDir: 'static',
lintOnSave: process.env.NODE_ENV === 'development',
productionSourceMap: false,
devServer: {
// 开发服务器的配置
port: port,
open: true,
overlay: {
warnings: false,
errors: true
},
proxy: {
// 给 /api 添加代理
'/api': {
target: 'http://ihrm.itheima.net/',
changeOrigin: true // 开启跨域
/* pathRewrite: {
'^/api': '' // 路径重写,也就是将路径中的/api替代成空字符串
} */
}
}
}
}
```
### 跨域
> 开发环境的跨域:后端提供的接口没有开启```cors```跨域,因此前后端分离的话,无法从本地访问到后端提供的接口中获取相关数据。但是在```vue-cli```脚手架在本地自动开了一个服务,因此可以利用后端的这个服务来实现向后端接口进行数据传输,实现跨域问题,这就是反向代理
配置
```js
//在vue.config.js中进行配置
devServer:{
proxy:{
// /api是表示我们的请求地址中如果存在有/api时,将会触发这个机制
// localhost:8080/api/abc => www.baidu.com/api/abc
// 本地前端 => 本地后端 => 服务器
'/api':{
targer: 'www.baidu.com', //反向代理的地址
changeOrigin: true, //开启跨域
pathRewrite:{ //路径重写
// localhost:8888/api/login => www.baidu.com/api/loin,默认会加上/api
'/api': ''
localhost:8888/api/login => www.baidu.com/loin
}
}
}
}
```
## 登录页面
### 进行相关的样式布局以及数据修改
- css样式中引入图片:css中需要使用```@```别名时,需要添加```~```,否则不能识别
```css
background-image: url('~@/assets/common/login.jpg');
```
- ```native```:@keyup.enter.**`native`** 表示监听组件的原生事件,比如 ```keyup```就是input的原生事件,这里写```native```表示```keyup```是一个原生事件,```el-input```外边包了一层```div```,根元素上监听原生事件
```vue
### 封装请求
```js
// 在api/user文件夹中进行axios的封装
export function login(data) {
return request({
url: '/sys/login',
method: 'POST',
data
})
}
```
```ba
# .env.development中配置基地址
VUE_APP_BASE_API = '/api'
```
```js
// 在request中进行拦截响应器的封装 utils/request
import {Message} from 'element-ui'
const request = axios.create({
baseURL: process.env.VUE_APP_BASE_API //在.env.development中配置基地址
})
request.interceptors.response.use(response=>{
// axios将返回回来的数据默认报上一层data对象,因此需要进行对象解构
// 上图是后端接口返回来的数据格式
const {success,code,message.data} = response
if(success) {
//将解构后的数据返回
return data
}else {
Message.error(message)
// 返回一个拒绝态的Promise
return Promise.reject(message)
}
},error=>{
Message.error(message)
// 返回一个拒绝态的Promise
return Promise.reject(message)
})
```
```js
// utils auth
import Cookies from 'js-cookie'
const TokenKey = 'vue_admin_template_token'
export function getToken() {
return Cookies.get(TokenKey)
}
export function setToken(token) {
return Cookies.set(TokenKey, token)
}
export function removeToken() {
return Cookies.remove(TokenKey)
}
```
```js
// 在vuex中进行数据请求 模块化 modules/user
import {login} from '@/api/user'
import {getToken, setToken, removeToken} from '@/utils/auth'
const actions = {
async getLogin(ctx,data){
const results = await login(data)
ctx.commit('updateToken',results)
}
}
const state = {
token: getToken()
}
const mutations = {
updateToken(state,token){
state.token = token
setToken(token)
}
}
export default {
state,
mutations,
actions
}
```
```vue
// 在login.vue中进行点击触发请求
> 在用户进行路由跳转时对```token```进行判断,跳转不一样的路由将会执行不一样的操作
```js
// permission.js 路由守卫
import router from '@/router'
import store from '@/store'
import nProgress from 'nprogress'
import 'nprogress/nprogress.css'
// 存放白名单
const whitePath = ['/login','404']
// 路由前置守卫
router.eachBefore((to,from,next)=>{
// 开启加载开始
nprogress.start()
// 存在有token
if(store.getters.token) {
// 判断用户跳转的路由地址
if(to.path === '/login') {
// 有Token的用户跳转到登录页面,将执行免登录操作,直接跳转到主页
next('/')
}
else {
// 去往其他页面将直接放行,让其通过
next()
}
}
// 没有token
else {
// 如果用户跳转的路由是在白名单中,即加载页面不需要token [].indexOf(path):如果path在数组中将返回从0开始的索引,不存在则返回-1
if(whitePath.indexOf(to.path) > -1){
// 直接放行
next()
}else {
// 如果不再白名单中将跳转到登录页面
next('/login')
}
}
})
router.eachAfter((to,from,next)=>{
// 关闭加载
nprogress.done()
})
```
### 获取用户资料
> 在用户跳转时,只有有token才需要进行用户资料的获取,并且如果不进行用户资料存在与否的判断时,将在每次在除登录跳转时都会进行数据请求,造成多次无效的网络请求
```js
// 完善访问权限拦截的逻辑
router.eachBefore(async (to,from,next)=>{
if(store.getters.token) {
if(to.path === '/login') {
next('/')
}
else{
// 判断是否有用户资料
if(!store.getters.userId){
// 数据请求
await store.dispatch('user/getUserInfo')
}
next()
}
}
})
```
```js
// vuex 中的user.js
import {getUserInfo, getUserDetail} from '@/api/user'
const state = {
userInfo = {} // 不能用null,因为在geeters.js中进行了快捷访问,不能访问null中的数据,会报错
}
const mutations = {
updateUserInfo(state,userInfo) {
state.userInfo = userInfo
}
}
const actions = {
async getUserInfo(ctx){
const result1 = await getUserInfo() // 获取用户资料
const result2 = await getUserDetail(result1.userId) //获取用户细节资料,包含了头像
const result = {...result1,...result2} //浅拷贝
ctx.dispatch('updateUserInfo',result)
}
}
```
### 全局自定义指令
> 在获取用户头像时,可能会存在图片加载失败,图片源失效,需要设置一个默认的图片,使用到自定义指令
```js
// src/directives/index.js
// 组件中使用 v-imgSrc = 'url地址'
export const imgSrc = {
inserted(dom,option){
// 给所有的dom元素(img)添加错误事件
dom.onError = function(){
dom.src = option.value
}
}
}
```
```js
// 在main.js中的进行全局注册
import * As directives from '@/directive'
// Object.keys(directives) 获取上文件对象中的所有导出的对象的属性 ['imgSrc']
Object.keys(directives).forEach(item => {
Vue.directive(item,directives[item])
})
```
### Token 失效
> 用户的```token```并不是一直有效的,如果用户的Token已经失效了,则需要删除```vuex```仓库中的相关数据并且移除本地存储的数据
#### 手动判断
> 手动判断需要根据token的超时时间进行判断,利用```当前时间-获取token时登录的时间```与自定义的超时时间进行对比
```js
// store user.js
import {getTime, setTime, removeToken} from '@/auth'
const state = {
userInfo: {}
}
const mutations = {
updateToken(state,token){
state.token = token
setToken(token)
setTime(Date.now()) //存入当前的时间戳
},
// 清空token
delToken(){
removeToken()
},
// 清空用户资料
delUserInfo(state){
state.userInfo = {}
}
}
const actions = {
async getLogin(ctx,data){
const results = await login(data)
ctx.commit('updateToken',results)
},
// 清空用户资料和Token
logOut(ctx){
ctx.commit('delToken')
ctx.commit('delUserInfo')
}
}
```
```js
// auth.js
import Cookies from 'js-cookie'
const TimeStamp = 'vue_admin_template-time'
const TokenKey = 'vue_admin_template_token'
// 存入时间戳
export function setTime(time) {
return Cookies.set(TimeStamp,time)
}
// 获取时间戳
export functon getTime(){
return Cookies.get(TimeStamp)
}
// 清空token
export function removeToken(){
return Cookies.remove(TokenKey)
}
```
```js
// request.js
import store from '@/store'
import router from '@/router'
import {getTime} from '@/utils/auth'
import {Message} from 'element-ui'
// 自定义超时时间
const timeOut = 3000
// 请求拦截响应期
request.interceptors.request.use(config=>{
if(store.getters.token) {
// 手动判断token有没有超时
if(isTimeOut()) {
// 清空用户资料和token
store.dispatch('user/logOut')
// 跳转到登录页面
router.push('/login')
Message.error('Token失效,请重新登录')
return Promise.reject('Token失效,请重新登录')
}
// 注入token
config.headers['Authorization'] = `Bearer ${store.getters.token}`
}
},error =>{})
// 判断用户token有没有超时
function isTimeOut(){
return (Date.now() - getTime()) > timeOut
}
```
#### 被动判断
>
>
> 在数据请求时,根据后端返回的数据中的code码进行判断token有没有失效,10002则是失效的
```js
// request.js
// 响应拦截器
request.interceptors.response.use(response =>{
},
// 当响应回来的响应码大于2xx则进入这个回调函数中,小于则进到前面那一个
error=>{
// axios响应回来的数据包了一层data,使用链判断运算符?.
if(error ?. data ?. code === 10002) {
// token失效,进行清除用户资料和token
store.dispatch('user/logout')
router.push('/login')
Message.error('Token失效,请重新登录')
return Promise.reject('Token失效,请重新登录')
}
})
```
## 主页
### 登出
> 点击退出登录将会清除用户资料以及清空用户资料
```vue
右侧侧边栏的位置在代码中由路由控制,同时分为静态路由和权限路由。
#### 分配路由
1. 将侧边栏按需分配路由,建立独立的路由文件,路由模块化管理,同时建立独立的页面文件
2. 独立的权限路由中也需要使用到```layout```的主页面,因此将```Layout```作为一级路由
```js
export default {
path: '/department',
name: 'department',
// 一级路由
component: '@/layout',
children: [
{
// 权限路由默认路由
path: '',
components: '@/views/department',
// 路由元信息
meta: {
title: '组织架构',
icon: 'tree'
}
}
]
}
```
3. 将静态路由和权限路由进行合并,整合成临时静态总路由
```js
// router/index.js
import Router from 'vue-router'
import 所有权限路由对象 from './components/路由路径' //导入所有的权限路由
export const constantRoutes = [] // 包含了登录、首页、默认路由以及404
export const dynamicRoutes = [
所有权限路由对象
]
const router = () => new Router({
routes: [...constantRoutes,...dynamicRoutes]
})
```
#### 侧边栏显示
1. 将路由的元信息进行在侧边栏上显示。```this.$router.options```中获取的是路由列表
```vue
// sideBar index.js
3. 函数式组件:没有```this```上下文,没有管理任何状态(即不需要```data```),也没有监听任何传递给他的状态,也没有生命周期方法,,也没有实例,实际上他只是一个接受```props```的函数。意味着他无状态,没有响应式数据
```render```:渲染函数
```vue
// item组件控制每一个路由的图标与标题
```
### 企业架构
```vue
// 在app-main组件中的路由占位中显示相对应的路由页面
> 后端返回来的数据是对象数组的形式,我们需要进行转换成树形结构,通过观察对比发现,根节点的```pid=''```,子节点的```pid```和父节点的```id```相等,可以通过这个规律将后端返回来的数据进行处理转换
```js
export default dataToTreeData(list,nodeValue){
const arr = []
list.forEach(item => {
if(item.pid === nodeValue) {
// 使用递归,将父节点的id作为子节点的pid进行遍历循环查找
const children = dataToTreeData(list,item.id)
if(children.length > 0) {
// 将子节点追加到父节点的children属性中
item.children = children
}
// 将item 追加到arr数组中
arr.push(item)
}
})
// 只有当每一次循环结束之后才会返回arr,然后将返回的结果作为属性的追加
return arr
}
```
#### 组织架构的相关操作
##### 删除部门
>
点击删除按钮,将会删除对应的部门数据
```vue
// itemTree.vue
>
> 进行表单中的相关字段的校验,点击确定将手动校验表单,校验通过则将进行数据的提交,点击取消和关闭按钮将关闭会话框,并重置表单
1. 字段校验
```bash
# 部门名称(name):必填 1-50个字符 / 同级部门中禁止出现重复部门
# 部门编码(code):必填 1-50个字符 / 部门编码在整个模块中都不允许重复
# 部门负责人(manager):必填
# 部门介绍 ( introduce):必填 1-300个字符
```
```js
// 部门名称和部门编码的自定义校验规则
import { getAllList, addDepartment } from '@/api/department'
data(){
const checkDepartName = async (rule,value,callback) => {
const {depts} = await getAllList
// node是父组件传过来的当前节点的数据
const isRepeat = depts.filter(item => item.pid === this.node.id).some(item => item.name === value)
return isRepeat ? callback(new Error('部门名重复')): callback()
}
const checkDepartCode = async (rule,value,callback) => {
const {depts} = await getAllList
// node是父组件传过来的当前节点的数据
const isRepeat = depts.some(item => item.code === value)
return isRepeat ? callback(new Error('编码名重复')): callback()
}
}
```
2. 在点击负责人表单项时,进行数据请求,获取所有员工,进行页面渲染
3. 点击确定时,对表单进行手动校验(返回一个```Promise```),校验通过则可以进行数据添加
> 通过```sync```修饰符来进行会话框的关闭,也可以通过```v-model```来进行修改会话框的值(```value+input```)
```js
addDepart() {
this.$refs.form.validate(async isOk => {
if (isOk) {
this.loading = true
await addDepartment({ ...this.formData, pid: this.node.id })
// this.$emit('input', false)
this.$emit('addDepts')
// update:是固定形式,:后面的参数是需要进行修改的数据
this.$emit('update:isShowDiag', false)
// 以上在关闭会话框时将会自动调用会话框的close事件
this.loading = false
this.$message.success('添加部门成功')
}
// 关闭弹框将触发diag的close事件
})
}
// 父组件 父组件使用 :参数名.sync=参数值 的形式进行传递
// 可以使用v-model进行修改,父组件使用value来进行接收,并且自定义事件名为input,子组件中可以使用model来修改默认的属性名和事件名 model:{prop:'...',event: '...'}
>
> 会话框中将点击的部门中的相关数据重写到会话框中供用户进行编辑
1. 点击按钮,将当前节点传递给父组件,在父组件中调用会话页面中获取节点数据的方法,然后将会话框进行显示
```vue
```vue
```vue
##### 编辑角色
> 点击编辑角色,将获取该角色的相关信息,并存储到表单项对应的```model```中,进行表单数据的重写
>
>
##### 会话框中的相关操作
###### 确定
> 点击确定时,将根据是编辑还是新增进行不一样的数据请求,新增时,表单数据中存在有相关数据
```js
async addRole() {
this.loading = true
// 打开会话框
this.dialogVisible = true
// 手动校验表单数据
try {
await this.$refs.addForm.validate()
if (this.formData.id) {
await editRole({ ...this.formData, companyId: this.$store.getters.companyId })
} else {
await addRole(this.formData)
}
// 获取最新的角色列表进行渲染
this.getAllRoleList()
let message = '添加角色成功'
this.formData.id ? message = '修改角色成功' : message
this.$message.success(message)
// this.dialogVisible = false// 关闭会话框
} catch (error) {
console.log(error)
} finally {
this.loading = false
this.dialogVisible = false// 关闭会话框
}
}
```
###### 取消
> 点击取消或者点击关闭按钮,将会关闭会话框,并将校验规则置空,校验规则只影响到了```name```字段,关于```description```,因此需要将表单数据置空,当值下次打开时仍留有上次关闭的信息
```js
cancel() {
// 关闭会话框
this.dialogVisible = false
// 情况表单校验
this.$refs.addForm.resetFields()
// 重置数据
this.formData = {
name: '',
description: ''
}
}
```
### 员工
#### 布局

##### 顶部
> 顶端部分的结构在多个页面都有出现,可以使用组件进行复用
```vue
// PageTool.vue
```vue
> 导入```excel```数据的文件的组件,在```vue-element-admin```中已经提供了相关的组件 [代码地址](https://github.com/PanJiaChen/vue-element-admin/blob/master/src/components/UploadExcel/index.vue)。这个组件依赖```xlsx```这个包
1. 安装依赖包
```bash
# 版本不一致容易报错
npm i xlsx@0.16.8
```
2. 将```vue-element-admin```提供的导入功能的代码添加到新建的组件中,并进行代码的相关改造
3. 将这个公共组件注册成成全局组件
4. 将文件导入这个组件单独用一个页面进行显示,则需要进行路由(静态,无权限)添加以及页面的添加
5. 获取批量导入员工的接口,导入excel数据到页面
```js
success({ header, results }) {
// 将results中的键的中文全部替换成英文
// 判断到这个页面来的路由中传递的参数是否为user
if (this.$route.query.type === 'user') {
results.map(obj => {
const newObj = {}
Object.keys(obj).forEach(item => {
if (item === '入职日期' || item === '转正日期') {
newObj[this.keys[item]] = formatDate(obj[item], '/)')
} else {
newObj[this.keys[item]] = obj[item]
}
})
this.importData.push(newObj)
})
this.importEmployee()
this.$message.success('批量导入用户成功')
// 回到上一个路由页面中
this.$router.back()
}
},
// 批量导入员工
async importEmployee() {
await importEmployee(this.importData)
},
// 将excel时间转换成我们需要的格式
formatDate(numb, format) {
const time = new Date((numb - 1) * 24 * 3600000 + 1)
time.setYear(time.getFullYear() - 70)
const year = time.getFullYear() + ''
const month = time.getMonth() + 1 + ''
const date = time.getDate() - 1 + ''
if (format && format.length === 1) {
return year + format + month + format + date
}
return year + (month < 10 ? '0' + month : month) + (date < 10 ? '0' + date : date)
}
```
```vue
> 整体布局同新增角色和新增组织架构相似,都是使用```el-dialog```加上表单,在本小结中将不对关闭会话框进行重复描述
1. 整体布局
- 入职时间与转正时间
这一个组件使用的是```el-date-picker```
- 聘用形式
这里使用的是```el-select```和```el-option```,使用的数据来源于```hireType```
- 部门
这里使用的是```el-tree```,当点击了部门所对应的输入框之后,就将这个```el-tree```显示,并获取数据,相关操作同组织架构
2. 数据校验
```js
// 部门的数据校验的触发方式应该是 change,不能使用 blur
```
3. 提交数据
```js
// 点击确定之后提交数据,然后通知父组件进行数据修改,重新渲染页面,这里使用 $parent
async addEmployee() {
await this.$refs.form.validate()
this.loading = true
await addEmployee(this.formData)
this.loading = false
// this.$emit('update:dialogVisible', false)
// 通过$parent进行数据重新获取以及关闭会话框 this.$parent是指组件的父组件,只有是亲父子才能使用$parent获取最外面的父组件
this.$parent.getAllDetail()
this.$parent.dialogVisible = false
}
```
##### 员工操作
###### 删除
> 点击删除按钮,触发一个事件,并删除当前行,这个事件中的参数将传递一个id
```vue
> 点击查看,将会跳转到新的路由页面中,这个路由作为员工路由的子路由,并且将设置```hidden```字段,侧边栏需要显示主路由,其他的路由需要隐藏,这里```el-tabs```组件中的内容区域采用动态组件的方式```
- 布局静态页面,配置校验字段和校验规则
- 获取数据进行填充
- 点击更新将最新的数据进行提交
- 同时将顶端的数据进行重新渲染,调用```store```中的```user```模块中的```actions```
> 在后端服务器中存储的密码是密文格式,不能直接显示在输入框中,也无法进行解密,因此需要借助另一个参数来帮助进行提交,同时不将获取到的密码显示在输入框中
```js
formData: {
username: '',
password2: ''
}
// 点击提交时触发的方法
async saveUser(){
.....
await saveUser({...this.formData,password: this.formData.password2})
}
```
2. 个人详情和岗位信息
> 整体布局同登录账号设置,都是对表单数据进行操作布局渲染,在个人详情页中存在有一个图片上传的组件
3. 图片上传的组件
```vue
#### 页面布局
1. 页面顶部用封装好的组件```pageTool```
2. 主体部分采用```element-ui```中的表格,并且封装了有树级结构,按照```element-ui```中的文档说明,需要将获取到的数据中包含有```children```字段,即可使用封装好的数据递归转换的方法,然后进行数据填充渲染
3. 在操作权限中没有添加按钮,访问权限中可以进行添加。可以观察获得的数据的规律,```type```字段
#### 操作
##### 添加
1. 顶端的添加按钮,将数据传递到主体内容中,添加的是访问权限
2. 行的添加按钮,是添加的操作权限
3. 访问权限和操作权限的```type```和```pid```有区别,操作权限不需要传入```id```
4. 点击按钮之后,将弹出添加权限的弹出框
##### 编辑
1. 点击编辑按钮,将弹出和添加权限的弹出框一样的弹出框,只是编辑按钮会重写数据到弹出框中
2. 点击编辑按钮,会发送请求获取数据,此时存在有相关的字段,根据这些字段进行添加和编辑按钮的区分
##### 删除
> 点击删除,直接删除当前行的数据,当前行的```id```根据插槽进行传递
#### 权限拦截
> 根据用户资料进行响应的用户权限的拦截,将分为两个部分的设计,用户可以进行权限路由的跳转,同时侧边栏可以显示
1. 路由
> 将路由相关的权限设置在```vuex```中,并在路由前置导航时进行相关的路由添加
1. ```vuex```中配置权限路由
```js
import { constantRoutes, dynamicRouter } from '@/router'
const state = {
route: constantRoutes // 静态路由
}
const mutations = {
// 修改路由
updateRoute(state,route){
// 需要以静态路由作为基础进行添加,防止路由的污染
state.route = {...constantRoutes,...route}
}
}
const actions = {
// 过滤路由,判断用户中存在的权限路由与动态路由的相关性
filterRoute(context,menuArr){
// menuArr是在用户登录时,获得的权限路由数组,将所有的路由存放到一个数组中。需要使用到延展运算符,不能使用map,因为map会返回一个数组,这个返回的是整个路由数组的集合,应该使用forEach,然后将每次过滤出来的数组加到一个数组中
const routes = []
menuArr.forEach(item => {
const filterRoutes = dynamicRouter.filter(obj => obj.children[0].name === item)
// 将所有权限路由加到数组routes中
routes.push(...filterRoutes)
})
context.commit('updateRoute',routes) //便于右侧侧边栏的显示
return routes // 将权限路由数组返回
}
}
export default {
state,
mutations,
actions
}
```
2. 路由导航守卫进行权限拦截添加
```js
// 在获取到用户的资料之后,进行路由权限的判断,这个获取资料是在 vuex 中获得的,并将用户的资料进行返回
const {rules} = store.dispatch('user/getUser')
const routes = store.dispatch('permissions/filterRoute',rules.menus) // menus是用户的权限路由点数组
// 进行路由的添加,使用 addRoutes()方法 router.addRoutes([路由配置对象])
// router.addRoutes(routes),动态添加权限路由,但是需要添加404的路由,因为要将404的路由放在最后,防止在刷新时页面404
router.addRoutes([...routes,{ path: '*', redirect: '/404', hidden: true }])
// 此时需要进行指定页面的跳转,否则会报错
next(to.path)
```
2. 侧边栏的显示
> 在路由导航守卫时,获得了用户的权限路由(```调用了vuex,获得了权限路由```),将这个权限路由给到侧边栏
```js
// 侧边栏之前的做法
routes(){
// 直接获取路由器中的路由表数据
return this.$router.options.routes
}
```
```js
// 动态添加路由
...mapGetters(['routes']) // 直接获取用户的权限路由
```
### 首页
#### 页面设计
> 页面整体分为上下结构,然后下面部分分为左右两部分
```vue
工作日历