# 人力资源中台项目管理 **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**: 1 - **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 ``` - 使用```elememt-UI```进行登录页面的操作 ### 封装请求 ```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中进行点击触发请求 登录 ``` ```js import {mapActions} from 'vuex' export default { methods:{ ...mapActions(['user/getLogin']) handleLogin(async isOk=>{ //开启加载 this.loading = true if(isOk) { try{ this['user/getLogin'] //跳转到主页 this.$router.push('/') } catch(error){ }finally{ //不管是否错误,都会执行到这一步,将loading置为false this.loading = false } } }) } } ``` ### 访问权限拦截 > 在用户进行路由跳转时对```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 退出登录 ``` ```js methods: { logout(){ // 清token 和用户资料 this.$store.dispatch('user/logOut') // 跳转到登录页面 this.$router.push('/login') } } ``` ### 路由管理 右侧侧边栏的位置在代码中由路由控制,同时分为静态路由和权限路由。 #### 分配路由 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 ``` 2. ```sidebar-item```组件,在路由组件中的静态路由中,登录路由以及404路由规则中存在有```hidden```属性,并且为true,用这个属性来控制在侧边栏中的显示与隐藏 3. 函数式组件:没有```this```上下文,没有管理任何状态(即不需要```data```),也没有监听任何传递给他的状态,也没有生命周期方法,,也没有实例,实际上他只是一个接受```props```的函数。意味着他无状态,没有响应式数据 ```render```:渲染函数 ```vue // item组件控制每一个路由的图标与标题 ``` ### 企业架构 ```vue // 在app-main组件中的路由占位中显示相对应的路由页面 ``` #### 布局 > 组织架构的主页面中分为上下两种结构,并且呈现的是一样的结构,可以考虑使用插槽技术 ![image-20221215112918762](C:\Users\FIRE\AppData\Roaming\Typora\typora-user-images\image-20221215112918762.png) ```vue // 插槽技术 ``` ```vue // 主页面
``` 通过数据请求回来的数据 > 后端返回来的数据是对象数组的形式,我们需要进行转换成树形结构,通过观察对比发现,根节点的```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: '...'} ``` 4. 点击取消和关闭图标,将会关闭会话框并清空表单 ```vue 取消 ``` ##### 编辑部门 > 点击编辑部门将弹出会话框 > > > > 会话框中将点击的部门中的相关数据重写到会话框中供用户进行编辑 1. 点击按钮,将当前节点传递给父组件,在父组件中调用会话页面中获取节点数据的方法,然后将会话框进行显示 ```vue ``` 2. 在会话框组件中,将存有部门的相关信息,包括```id```,可以通过这个```id```来进行会话框标题的显示 ```js async getDeptsInfo(id) { // 将获取到的节点的数据传到formData中 this.formData = await getDepartment(id) } computed:{ // 会话框的标题 title(){ return this.formData.id ? '编辑部门' : '添加子部门' } } ``` 3. 会话框的```close```事件和添加子部门的事件一样 4. 点击确定后,需要根据是编辑操作还是添加操作,进行不一样的请求 ### 公司设置 #### 布局 ##### 角色管理 ###### 内容区域 ```vue
新增角色
``` ###### 底部分页区域 ![image-20221216195959894](C:\Users\FIRE\AppData\Roaming\Typora\typora-user-images\image-20221216195959894.png) ```vue ``` ##### 公司信息 ```vue ``` #### 相关操作 ##### 分页栏的切换操作 > 点击分页栏,将会显示不同页面的数据,分页栏中存在一个事件```current-change``` ```js // 点击下一页或上一页的页码 change(page) { // 修改页码 this.pageInfo.page = page // 重新获取数据 this.getAllRoleList() } ``` ##### 删除角色 > ```element-ui```中的```table```中可以使用插槽技术,用来获取表格行中的相关数据 ```vue // 通过 Scoped slot 可以获取到 row, column, $index 和 store(table 内部的状态管理)的数据 ``` ```js async delRole(id) { this.loadingTab = true try { // $confirm返回值是一个Promise await this.$confirm('确定是否删除该角色?', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }) await delRole(id) this.$message.success('删除成功!') // 删除成功之后需要重新获取角色列表 this.getAllRoleList() } catch (error) { console.log(error) } finally { this.loadingTab = false } } ``` ##### 新增角色 > 点击新增角色将会弹出一个会话框,供用户填写相关的数据用于新增角色,点击按钮只需要弹出会话框,新增的操作在点击确定按钮之后进行 > > ##### 编辑角色 > 点击编辑角色,将获取该角色的相关信息,并存储到表单项对应的```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: '' } } ``` ### 员工 #### 布局 ![image-20221219181126821](C:\Users\FIRE\AppData\Roaming\Typora\typora-user-images\image-20221219181126821.png) ##### 顶部 > 顶端部分的结构在多个页面都有出现,可以使用组件进行复用 ```vue // PageTool.vue ``` ##### 内容 > 内容区域同时分为两个部分(表格区域```el-table```和分页区域```el-pagination```),将获取到的数据进行相关的渲染填充 ```vue ``` ###### 相关格式的修改 1. 聘用形式 > 获取的数据中的聘用形式是数字,在相关的枚举数据中进行了相关数据的定义 ```js // utils/employee.js hireType: [ { id: 1, value: '正式' }, { id: 2, value: '非正式' } ] ``` ```vue ``` 2. 入职时间 > 将时间统一格式,使用全局过滤器进行格式化,这个时候需要使用插槽技术 ```vue ``` 3. 状态 > 状态可以使用```element-ui```里的```el-switch```组件 ```vue ``` 4. 操作 > 最后一列操作栏中,需要给每个按钮添加绑定事件,同时需要传响应的```id(每行的id)``` ```vue ``` #### 相关操作 ##### 顶端操作 ```vue ``` ###### 普通excel导出 > ```excel```的导入和导出都是依靠[js-xlsx](https://github.com/SheetJS/js-xlsx)实现的,在```js-xlsx```的基础上又封装了[Export2Excel.js](https://github.com/PanJiaChen/vue-element-admin/blob/master/src/vendor/Export2Excel.js)来导出数据 > > ![image-20221219201422386](C:\Users\FIRE\AppData\Roaming\Typora\typora-user-images\image-20221219201422386.png) 1. 安装所需要的的依赖 ```bash yarn add xlsx file-saver # 安装在运行依赖和开发依赖 yarn add script-loader -S -D ``` 2. 懒加载所需要的的依赖,```js-xlsx```体积过大,则可以点击了之后在进行加载 ```js // 当点击了按钮之后再进行加载模块 exportToExcel(){ import('@/vender/Export2Excel').then(excel => { excel.export_json_to_excel( { header: '', // 格式:[] data: '', // 格式:[[]] fileName: '',// 文件名 }) }) } ``` 相关参数设置如下: | 参数 | 说明 | 类型 | 可选值 | 默认值 | | ----------- | -------------------- | ------- | --------------- | ---------- | | header | 表头 | Array | / | [] | | data | 数据 | Array | / | [[]] | | fileName | 导出文件名称 | String | / | excel-list | | autoWidth | 单元格是否要适应宽度 | Boolean | true/false | true | | bookType | 文件扩展名 | String | xlsx,csv,txt... | slsx | | multiHeader | 复杂表头的部分 | Array | / | [[]] | | merges | 需要合并的部分 | Array | / | [] | 3. 获取相关参数 ```js // 在data中存放中英转换的key的数据 keysToEng: { '姓名': 'username', '工号': 'workNumber', '手机号': 'mobile', '聘用形式': 'formOfEmployment', '入职日期': 'timeOfEntry', '转正日期': 'correctionTime', '部门': 'departmentName' } ``` ```js // 获取data,调用获取所有员工列表的接口,不能使用组件中的单页面的数组,因为存放的只有一页的数据,应该将页面设为一页,同时将每页的数据设置为动态的,使用数据总条数进行获取 const { rows } = await getAllDetail( { page: 1, size: this.pageInfo.total } ) ``` ```js // 获取到的数据的格式是 [{}],但是导出的数据中需要的格式是 [[]],则需要进行相应的转换 formatData(data){ return data.map(item => { return Object.keys(this.keysToEng).map(value => { if(value === '入职日期' || value === '转正日期'){ // 使用上述布局中用到过滤器方法,进行日期的格式化 return formatDate(item[this.keysToEng[value]]) }else if(value === '聘用形式'){ var exact = EmployeeEnum.hireType.find(obj => obj.id === item[headers[key]]) return exact ? exact.value : '未知' } return item[this.keysToEng[value]] }) }) } ``` ```js // 整合上述两段代码,获取全部员工数据之后,就直接进行数据的格式转换 async getAllInfo(){ const {rows} = await getAllDetail({ page: 1, size: this.pageInfo.total }) return this.formatData(rows) } exportToExcel(){ import('@/vender/Export2Excel').then(async excel => { // async返回的是一个Promise const data = await this.getAllInfo() excel.export_json_to_excel({ header: Object.keys(this.keysToEng), data:, fileName:'员工信息表' }) }) } ``` ###### 复杂excel导出 > 导出一个复杂的```excel```表格,即表头有多行并存在有合并,前民的步骤和普通```excel```导出一样,只是在导出时的参数设置时不一样,多了```multiHeader```和```mergers``` > > ![image-20221219201404212](C:\Users\FIRE\AppData\Roaming\Typora\typora-user-images\image-20221219201404212.png) ```js comExportToExcel(){ import('@/vender/Export2Excel').then(async excel => { // async返回的是一个Promise // 在定义复合表头时,如果不存在相关字段,也需要写空字符串 const multiHeader = ['姓名','主要信息','','','','','部门'] const merges = ['A1-A2','B1-F1',G1-G2] const data = await this.getAllInfo() excel.export_json_to_excel({ multiHeader, merges, header: Object.keys(this.keysToEng), data:, fileName:'员工信息表' }) }) } ``` ###### excel导入 > 导入```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 excel导入 ``` ###### 新增员工 > 整体布局同新增角色和新增组织架构相似,都是使用```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```组件中的内容区域采用动态组件的方式`````` 1. 登录账号设置 - 布局静态页面,配置校验字段和校验规则 - 获取数据进行填充 - 点击更新将最新的数据进行提交 - 同时将顶端的数据进行重新渲染,调用```store```中的```user```模块中的```actions``` > 在后端服务器中存储的密码是密文格式,不能直接显示在输入框中,也无法进行解密,因此需要借助另一个参数来帮助进行提交,同时不将获取到的密码显示在输入框中 ```js formData: { username: '', password2: '' } // 点击提交时触发的方法 async saveUser(){ ..... await saveUser({...this.formData,password: this.formData.password2}) } ``` 2. 个人详情和岗位信息 > 整体布局同登录账号设置,都是对表单数据进行操作布局渲染,在个人详情页中存在有一个图片上传的组件 3. 图片上传的组件 ```vue ``` ### 权限管理 > 权限受控的思想:利用```RBAC(role-based-access-control)```,不用给每个用户添加拥有的相应权限,可以引入角色的相关概念,通过给用户分发角色,然后给角色分发权限,即完成了给用户分发权限的作用 #### 页面布局 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 ``` #### 日历 ```vue

工作日历

``` #### 雷达图 > 雷达图需要使用```echart```图标 1. 安装```echart``` ```npm i echarts``` 2. 配置相关参数 ```vue ``` ## 优化 ### 全屏 ```js // 原生中全屏的方案 document.documentElement.requestFullscreen() document.exitFullscreen() ``` ```js // 使用插件 npm i screenfull import screenFull from 'screenfull' if(!screenFull.isEnabled) { this.$message.warning('全屏不可用') return } screenFull.toggle() ``` ### 主题切换 > [项目地址]: https://github.com/PanJiaChen/vue-element-admin/blob/master/src/components/ThemePicker/index.vue "项目地址" ### 多语言 1. 下载插件 ```vue-i18n```(可能会有版本问题,导致不能进行全局注册) 2. 相关步骤同```Router``` ```js import Vue from 'vue' import VueI18n from 'vue-i18n' import Cookies from 'js-cookies' import elementEN from 'element-ui/lib/locale/lang/en' // 引入饿了么的英文包 import elementZH from 'element-ui/lib/locale/lang/zh-CN' // 引入饿了么的中文包 // 自定义的语言包 import zh from './zh' import en from './en' Vue.use(VueI18n) export default new VueI18n({ locale:Cookies.get('language') || 'zh', //从Cookies中获取相关语言的设置 message: { // 语言包由element-ui和自定义两方面组成 zh:{ ...elementZH, ...ah }, en:{ ...elementEN, ...en }, } }) ``` 3. 在```main.js```中进行注册 ```js import i18n from './lang' Vue.use(ElementUI, { i18n: (key, value) => i18n.t(key, value) }) new Vue({ el: '#app', i18n, render: h => h(App) }) ``` 4. 创建一个便于切换语言的组件,利用```element-ui```的下拉 ```vue ``` 5. 侧边栏的多语言显示,侧边栏的显示由路由表中的```meta```设置 > 注册好```i18n```之后,可以获得```$t```的方法,这个方法支持嵌套,即可以通过```.```获取相关的文本 ```vue ``` ### tags标签 ![image-20221227163128013](C:\Users\FIRE\AppData\Roaming\Typora\typora-user-images\image-20221227163128013.png) > 调用```vue-element-admin```中的组件,已经写好的内容,需要引用组件和vuex中的数据 ## 部署