定制app开发B站云E办Springboot+vue——前端项目完整版(含源码)

一、项目简介

项目背景:定制app开发受疫情的影响,定制app开发许多企业由线上办公转定制app开发为线下办公。定制app开发随着线上办公的人数的增多,定制app开发线上办公的优点逐步凸显:定制app开发通过实现工作流程的自动化、定制app开发节省企业办公费用、定制app开发实现绿色办公,定制app开发同时提升办公效率。

项目介绍:定制app开发本项目实现了一个在线办公系统,定制app开发用来管理日常办公事物的:定制app开发日常流程审批,新闻,通知,公告,文件信息,财务,人事,费用,资产,行政,项目,定制app开发移动办公等。定制app开发通过软件的方式让办公定制app开发系统根据方便管理,定制app开发提高整体的管理运营水平。

实现方式:定制app开发本项目基于Vue+Spring Boot构架一个。定制app开发前端使用社区非常活跃定制app开发的开源框架vue进行构建。简单地说,定制app开发前后端分离 定制app开发的核心思想是前端页面通过 ajax 定制app开发调用后端的 restuful api 定制app开发进行数据交互,而 定制app开发单页面应用(single page web application,),定制app开发就是只有一张页面,定制app开发并在用户与应用程序交定制app开发互时动态更新该页面的 Web 应用程序。

1.1 技术架构

1.2 定制app开发前端技术架构

定制app开发本项目采用前后端分离开发模式,使用Spring Boot构建后端。

定制app开发前端使用的技术有:

Vue、Vue-cli、Vuex、VueRouter、ElementUI、Axios、ES6、Webpack、WebSocket、font-awesome、js-file-download、vue-chat

项目搭建:Vue-cli ;状态管理:Vuex ;路由管理:VueRouter;UI界面:ElementUI;通讯框架:Axios ;

前端语法:ES6;打包工具:Webpack;在线聊天:WebSocket;字体:font-awesome;定制app开发文件上传下载:js-file-download;定制app开发在线聊天开源项目:vue-chat

定制app开发前端模块分为:登录、职位管理、职称管理、部门管理、操作员历、员工管理、定制app开发工资账套管理、个人中心、在线聊天

1.3 云E办(前端)

定制app开发前后端之间通过 RESTful API 传递 JSON 定制app开发数据进行交流。不同于 JSP 之类,定制app开发后端是不涉及页面本身的内容的。定制app开发在开发的时候,定制app开发前端用前端的服务器(Nginx),定制app开发后端用后端的服务器(Tomcat),定制app开发当我开发前端内容的时候,定制app开发可以把前端的请求通过定制app开发前端服务器转发给后端(称为反向代理),定制app开发这样就能实时观察结果,定制app开发并且不需要知道后端怎么实现,定制app开发而只需要知道接口提供的功能。

目录


二、Vue.js框架

Web定制app开发前端开发从开始到兴起再到如今,定制app开发已经发展了很多年,定制app开发同时累积了非常多的开定制app开发发经验和开发工具。定制app开发过去的开发者们的一些定制app开发经验也都经过了技术发展,定制app开发环境变化等重重考验,定制app开发他们所创造出来的思想、定制app开发技术和工具,定制app开发也非常值得我们后来开定制app开发发者的借鉴和学习,定制app开发甚至直接拿来使用。定制app开发因为开发工具和开发语定制app开发言不管怎么发展,定制app开发不管差异有多大,定制app开发但是他们所解决的问题定制app开发都是相似而统一的,定制app开发可以归纳为:

(1)扩充JavaScript、html和CSS定制app开发三种编程语言本身的语言能力;

(2)定制app开发解决开发过程中重复的工作;

(3)定制app开发将项目进行模块化;

(4)定制app开发解决功能复用和变更问题;

(5)定制app开发解决开发和产品环境差异问题;

(6)定制app开发解决发布流程问题。

定制app开发为了解决上述问题便产定制app开发生了工程化的思想,定制app开发工程化就是这种避免重定制app开发复造轮子的最好实践。Vue.js定制app开发是一套构建用户界面的定制app开发渐进式框架,定制app开发它是由中国程序开发者尤雨溪在2013年开发。因为Vue.js定制app开发简洁的语法设计、定制app开发轻量快速的特点在技术定制app开发社区中深受开发者欢迎,定制app开发因而也促进了Vue.js定制app开发的推广和流行。Vue.js定制app开发在相关的工具和支持库定制app开发配合使用下,定制app开发也能完美地驱动复杂的单页应用,定制app开发开发一个大型的Web应用。Vue.js在权威的JavaScript定制app开发趋势榜上已经蹿升到了总榜的前30定制app开发位且能持续的排在榜单的前列,定制app开发显然已成为一个全球顶尖的JavaScript框架。Vue.js定制app开发的生态不仅体现在趋势榜上,定制app开发其配套的数据管理库vuex、定制app开发路由管理库Vue-router、打包工具、定制app开发开发者调试插件和项目定制app开发脚手架等工具和库也都定制app开发逐渐开发成型,定制app开发同样也拥有非常活跃的技术社区。

Vue.js定制app开发是一个轻量级的MVVM前端框架,定制app开发可以用于构建渐进式用户界面。开发者在Vue.js定制app开发中构建前端页面时,定制app开发只用关心页面逻辑的实现。Vue.js定制app开发最大的特点是由底层逐定制app开发层向上应用,定制app开发不仅易上手还能兼容大量的第三方库。

2.1Vue特性

虚拟DOM

vue区别于传统框架的特点一是虚拟DOM。浏览器进行DOM操作会带来较大的开销,因此在Vue中通过diff算法构建了Virtual DOM,数据每次更新时比对最小变化,重新构建Virtual DOM。

响应式

Vue.js的核心被设计为一个响应的数据绑定系统,因此可以非常方便的将数据与DOM保持同步。在使用jQuery手工操作DOM时往往容易编写命令式的、重复的并且易错的代码。而Vue.js拥抱数据驱动的视图概念意味着在普通HTML模板中使用特殊的语法将DOM“绑定”到底层数据。这种绑定一旦创建,DOM便与数据实现了保持同步。每当对数据进行了修改则会相应的更新DOM。通过这种方式,在开发应用中,所有的业务逻辑就几乎只用直接修改数据,而不必与对DOM进行单独的更新操作,使得数据和DOM更新不会搅合在一起。这也让应用的代码更容易撰写、理解与维护。

组件化

在大型的应用开发中,往往会将应用抽象为多个相对独立的模块,目的是为了代码块的可复用性和维护性。然而只有当考虑复用性的时候才会将某一模块做成单独的一个组件,实际上,Web的视图界面也完全可以分为一个组件树。组件化是Vue.js最强大的功能之一。组件可以将视图页面的标签元素进行扩展然后进行封装,最终变成可重复使用的代码。从高层面上理解,组件也可以是一个自定义的元素,然后通过Vue.js的编译器的编译,可以为这个元素添加某种特殊的功能。同时,组件也可以是原生的HTML元素,通过is特性扩展。Vue.js和同样强调组件化思想的前端框架React.js有些类似,但是要比其更加轻量,简洁和先进。

组件化通常是指Vue.js能够将JavaScript代码、超文本标记语言(hypertext markup language, HTML)代码和层叠样式表 (cascadingstyle sheets, CSS)代码写在同一个文件里。开发者在实际开发中常常会遇到页面的功能需要多次使用的情况,这时可以在components目录下,构建可复用的组件。如果其他页面需要使用该组件,那么可以通过import方法进行引入。由于页面由多个组件构成,组件与组件之间耦合度较低,可大量减少重复性代码。

局部刷新

vue是一个,单页面应用的主要特性就是网页的局部刷新,网页应用通过控制控制路由调用AJAX,后台只需要提供接口即可实现。这样的应用优势明显,首先在用户体验上会更人性化,不需要刷新整个页面,因此加载速度快速,体验更好。

2.2 MVVM设计模式

基于B/S架构的Java Web应用系统在被开发时,前端页面的绘制与美化是系统开发的重要工作。页面的绘制与交互一般是基于对文档对象模型(document object model, DOM)元素节点和数据的操控来完成的,但直接操作DOM节点极易产生错误。近年来,随着前端技术的发展,涌现了各种各样的前端框架,这些框架基于MVVM (Model-View-ViewModel)设计模式,为前端工程的开发与维护带来了许多便利 。MVVM设计模式基于传统的MVC设计模式衍生而来,全称为Model-View-ViewModel。Model层负责持有用户数据,View层负责在屏幕上显示视觉元素和控件,ViewModel层负责将模型转换为可在视图上直接显示的值。

2.3 vue相关组件

脚手架vue-cli

快速开发工具vue-cli。它可以帮助开发者基于vue.js框架进行快速开发。vue-cli将各种工具标准化,确保各种构建工具能够基于智能的默认配置平稳衔接,使开发者在撰写前端应用时更加专注,不必花费很长时间去调整项目配置。

路由Vue-router

Vue.js构建的单页面的Web应用需要基于路由和组件。其中,路由的主要作用是用来设定访问路径,并将访问路径和视图组件相映射起来。在单页面的Web应用里,路径之间的跳转和切换其实是对应组件之间的切换。和React.js一样,Vue.js自身是也是不具有路由功能的。因此,在Vue.js框架使用的时候需要与一个路由工具库相互协作,即Vue-router。Vue-router能够将不同层级并且嵌套的路由关系映射到相应的嵌套的组件,并且提供了一个细致的控制路径跳转的解决方式。

状态管理vuex

Vue.js的视图变化都基于组件的状态,所以当构建大型的Web应用的时候会产生大量的组件状态,从而需要对这些状态进行管理。由于vue的“单向数据流”无法解决多个组件共享状态的问题,所以才有了vuex。Vuex便是一个集中式存储和管理应用的所有组件的状态的管理架构,专门为配合使用了Vue.js框架的应用而设计。它借鉴了React.js的状态管理工具Flux和Redux的设计理念,并对一些概念进行了简化,从而能更好的发挥Vue.js的数据响应机制。

通信框架axios

通信框架。因为vue的边界很明确,就是为了处理DOM,所以不具备通信能力,此时就需要额外使用一个通信框架与服务器交互;当然也可以直接使用jQuery提供的AJAX通信功能。经典的Ajax技术实现了网页的局部数据刷新,而Axios又对Ajax进行再次封装,它具备如下特征:

(1)从浏览器中创建XMLHttpRequest

(2)从node.js中发出http请求

(3)支持PromiseApi

(4)拦截请求和响应

(5)转换请求和相应数据

(6)取消请求

(7)自动转换JSON数据

(8)客户端支持防止CSRF/XSRF

Axios插件很好的封装了Ajax技术,在项目开发中写法简洁明了,因此不容易出错,即使出错也易于排查。

打包工具webpack

前端开发和其他开发工作的主要区别,首先是前端基于多语言、多层次的编码和组织工作,其次前端产品的交付是基于浏览器的,这些资源是通过增量加载的方式运行到浏览器端,如何在开发环境组织好这些碎片化的代码和中资源,并且保证他们在浏览器快速、优雅的加载和更新,就需要一个模块化系统。

webpack是一个大型js应用程序的模块化工具,会自动处理命名空间等一系列js编程遇到的问题。当webpack处理js的应用程序时,它会构建一个复杂的依赖关系图,这个关系图中包含了应用程序依赖的模块,甚至包含了很多静态资源,然后webpack会将这些模块打包成一个或多个大的模块,在应用程序中引用。

ES6模块

vue通常用es6来写,用export default导出,其下面可以包含数据data,生命周期(mounted等),方法(methods)等,具体语法请看vue.js文档。ES6标准增加了javascript语言层面的模块体系定义。ES6模块的设计思想,是尽量静态化,使编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS和AMD模块,都只能在运行时确定这些东西。

UI框架

ElementUI,饿了么出

三、搭建vue.js项目

3.1 搭建vue.js项目

环境准备

安装Node.js(>=6.x,首选8.x)本项目是v14.18.0版本

安装 Vue CLI

因为需要使用 npm 安装 Vue CLI,而 npm 是集成在 Node.js 中的,所以第一步我们需要安装 Node.js,访问官网 ,首页即可下载。

下载完成后运行安装包,一路下一步就行。

然后在 cmd 中输入 node -v,检查node是否安装成功。

输入 npm -v 查看npm版本号

输入 npm -g install npm ,将 npm 更新至最新版本。

之后,使用 npm install -g vue-cli 安装脚手架。(本项目使用版本2.9.6)

注意此种方式安装的是 2.x 版本的 Vue CLI,最新版本需要通过 npm install -g @vue/cli 安装。新版本可以使用图形化界面初始化项目,并加入了项目健康监控的内容,但使用新版本创建的项目依赖与这个教程不太相符,折腾起来比较麻烦。

安装Node.js的淘宝镜像加速器cnpm

大部分情况使用npm,遇到安装不了的使用cnpm

npm install cnpm -g

或npm install --registry=https://registry.npm.taobao.org

3.2 构建前端项目

通用方法

直接使用命令行构建项目。

然后执行命令 vue init webpack yeb,这里 webpack 是以 webpack 为模板指生成项目,还可以替换为 pwa、simple 等参数,这里不再赘述。

在程序执行的过程中会有一些提示,可以按照默认的设定一路回车下去,也可以按需修改,比如下图问我项目名称是不是 wj-vue,直接回车确认就行。

这里还会问是否安装 vue-router,一定要选是,也就是回车或按 Y,vue-router 是我们构建单页面应用的关键。

还有是否使用 es-lint,选N。

接下来等待项目构建完成就 OK 了。

可以看到 workspace 目录下生成了项目文件夹 需要在该文件夹执行 npm install ,npm run build 再执行 npm run dev

访问 ,查看网页 demo,大工告成!

注:在vue项目中,有的时候需要执行npm run serve启动项目,有的时候需要用npm run dev,具体有什么不一样呢?

区别

dev默认是-cli@2.x默认支持的命令;

serve默认是vue-cli@3.x及以上版本默认支持的命令。

3.3 vue项目结构分析

  1. ├── build --------------------------------- 项目构建(webpack)相关配置文件,配置参数什么的,一般不用动
  2. │ ├── build.js --------------------------webpack打包配置文件
  3. │ ├── check-versions.js ------------------------------ 检查npm,nodejs版本
  4. │ ├── dev-client.js ---------------------------------- 设置环境
  5. │ ├── dev-server.js ---------------------------------- 创建express服务器,配置中间件,启动可热重载的服务器,用于开发项目
  6. │ ├── utils.js --------------------------------------- 配置资源路径,配置css加载器
  7. │ ├── vue-loader.conf.js ----------------------------- 配置css加载器等
  8. │ ├── webpack.base.conf.js --------------------------- webpack基本配置
  9. │ ├── webpack.dev.conf.js ---------------------------- 用于开发的webpack设置
  10. │ ├── webpack.prod.conf.js --------------------------- 用于打包的webpack设置
  11. ├── config ---------------------------------- 配置目录,包括端口号等。我们初学可以使用默认的。
  12. │ ├── dev.env.js -------------------------- 开发环境变量
  13. │ ├── index.js ---------------------------- 项目配置文件
  14. │ ├── prod.env.js ------------------------- 生产环境变量
  15. │ ├── test.env.js ------------------------- 测试环境变量
  16. ├── node_modules ---------------------------- npm 加载的项目依赖模块
  17. ├── src ------------------------------------- 我们要开发的目录,基本上要做的事情都在这个目录里。
  18. │ ├── assets ------------------------------ 静态文件,放置一些图片,如logo等
  19. │ ├── components -------------------------- 组件目录,存放组件文件,可以不用。
  20. │ ├── main.js ----------------------------- 主js
  21. │ ├── App.vue ----------------------------- 项目入口组件,我们也可以直接将组件写这里,而不使用 components 目录。
  22. │ ├── router ------------------------------ 路由
  23. ├── static ---------------------------- 静态资源目录,如图片、字体等。
  24. ├── .babelrc--------------------------------- babel配置文件
  25. ├── .editorconfig---------------------------- 编辑器配置
  26. ├── .gitignore------------------------------- 配置git可忽略的文件
  27. ├── index.html ------------------------------ 首页入口文件,你可以添加一些 meta 信息或统计代码啥的。
  28. ├── package.json ---------------------------- node配置文件,记载着一些命令和依赖还有简要的项目描述信息
  29. ├── .README.md------------------------------- 项目的说明文档,markdown 格式。想怎么写怎么写,不会写就参照github上star多的项目,看人家怎么写的

主要文件详解

src——[项目核心文件]

在vue-cli的项目中,其中src文件夹是必须要掌握的,因为基本上要做的事情都在这个目录里。

index.html——[主页]

index.html如其他html一样,但一般只定义一个空的根节点,在main.js里面定义的实例将挂载在根节点下,内容都通过vue组件来填充,构建的文件将会被自动注入,也就是说我们编写的其它的内容都将在这个 div 中展示。整个项目只有这一个 html 文件,所以这是一个 单页面应用,当我们打开这个应用,表面上可以有很多页面,实际上它们都只不过在一个 div 中。

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8">
  5. <title>vuedemo</title>
  6. </head>
  7. <body>
  8. <!-- 定义的vue实例将挂载在#app节点下 -->
  9. <div id="app"></div>
  10. </body>
  11. </html>

App.vue——[根组件]

这个文件称为“根组件”,因为其它的组件又都包含在这个组件中。.vue 文件是一种自定义文件类型,在结构上类似 html,一个 .vue 文件即是一个 vue 组件。

一个vue页面通常由三部分组成:模板(template)、js(script)、样式(style)

<!-- 模板 -->

  1. <template><template>
  2. <div id="app">
  3. <img src="./assets/logo.png">
  4. <router-view></router-view>
  5. </div>
  6. </template>
  7. <!-- script -->
  8. <script>
  9. export default {
  10. name: 'app'
  11. }
  12. </script>
  13. <div id="app">
  14. <img src="./assets/logo.png">
  15. <router-view></router-view>
  16. </div>
  17. </template>
  18. <!-- script -->
  19. <script>
  20. export default {
  21. name: 'app'
  22. }
  23. </script>

<!-- 样式 -->

  1. <style>
  2. #app {
  3. font-family: 'Avenir', Helvetica, Arial, sans-serif;
  4. -webkit-font-smoothing: antialiased;
  5. -moz-osx-font-smoothing: grayscale;
  6. text-align: center;
  7. color: #2c3e50;
  8. margin-top: 60px;
  9. }
  10. </style>

【template】

其中模板只能包含一个父节点,也就是说顶层的div只能有一个(例如上面代码,父节点为#app的div,其没有兄弟节点)。这里也有一句 <div id="app">,但跟 index.html 里的那个是没有关系的。这个id=app 只是跟下面的 css 对应。

<router-view></router-view>是子路由视图,后面的路由页面都显示在此处。打一个比喻吧,<router-view>类似于一个插槽,跳转某个路由时,该路由下的页面就插在这个插槽中渲染显示

【script】

<script>标签里的内容即该组件的脚本,也就是 js 代码,export default 是 ES6 的语法,意思是将这个组件整体导出,之后就可以使用 import 导入组件了。大括号里的内容是这个组件的相关属性。

vue通常用es6来写,用export default导出,其下面可以包含数据data,生命周期(mounted等),方法(methods)等,具体语法请看vue.js文档。

【style】

样式通过style标签包裹,默认是影响全局的,如需定义作用域只在该组件下起作用,需在标签上加scoped.

如要引入外部css文件,首先需给项目安装css-loader依赖包,打开cmd,进入项目目录,输入npm install css-loader,回车。

安装完成后,就可以在style标签下import所需的css文件,例如:

  1. <style>
  2. import './assets/css/public.css'
  3. </style>

main.js——[入口文件]

main.js主要是引入vue框架,根组件及路由设置,并且定义vue实例,下面的 components:{App}就是引入的根组件App.vue。后期还可以引入插件,当然首先得安装插件。

前面我们说 App.vue 里的<div id="app"> 和 index.html 里的<div id="app"> 没有关系,那么这两个文件是怎么建立联系的呢?让我们来看入口文件 main.js 的代码

  1. /*引入vue框架*/
  2. import Vue from 'vue'
  3. /*引入根组件*/
  4. import App from './App'
  5. /*引入路由设置*/
  6. import router from './router'
  7. /*关闭生产模式下给出的提示*/
  8. Vue.config.productionTip = false
  9. /*定义实例*/
  10. new Vue({
  11. el: '#app',
  12. router,
  13. template: '<App/>',
  14. components: { App }
  15. })

最上面 import 了几个模块,其中 vue 模块在 node_modules 中,App 即 App.vue 里定义的组件,router 即 router 文件夹里定义的路由。

Vue.config.productionTip = false ,作用是阻止vue 在启动时生成生产提示。

在这个 js 文件中,我们创建了一个 Vue 对象(实例),el 属性提供一个在页面上已存在的 DOM 元素作为 Vue 对象的挂载目标,这里就通过index.html中的<div id="app"><div>中的id=“app”和这里的“#app”进行挂载。

router 代表该对象包含 Vue Router,并使用项目中定义的路由。components 表示该对象包含的 Vue 组件,template 是用一个字符串模板作为 Vue 实例的标识使用,类似于定义一个 html 标签。

3.4 安装 Element-UI

Element 的官方地址为

1.安装 Element

根据官方文档的描述,在项目文件夹下,执行 npm i element-ui -S 即可

在这里插入图片描述

2.引入 Element

引入分为完整引入和按需引入两种模式,按需引入可以缩小项目的体积,这里我们选择完整引入。

根据文档,我们需要修改 main.js 为如下内容

import ElementUI from 'element-ui'

import 'element-ui/lib/theme-chalk/index.css'

3.5 安装axios

进入到项目文件夹中,执行

npm install --save axios,以安装这个模块。

3.6 安装Vuex

Vuex,它是专门为 Vue 开发的状态管理方案,我们可以把需要在各个组件中传递使用的变量、方法定义在这里。之前我一直没有使用它,所以在不同组件传值的问题上十分头疼,要写很多多余的代码来调用不同组件的值,所以推荐大家从一开始就去熟悉这种管理方式。

运行 npm install vuex --save

之后,在 src 目录下新建一个文件夹 store,并在该目录下新建 index.js 文件,在该文件中引入 vue 和 vuex,代码如下:

import Vue from 'vue'

import Vuex from 'vuex'

Vue.use(Vuex)

安装vuex 启动 报错 “export ‘watch‘ was not found in ‘vue‘

如果你的vue版本是 2.X ,将vuex升到 3.X.X 就能够解决

npm install --save vuex@3.6.2

如果你的vue版本是 3.X ,将vuex升到 4.X.X 就能够解决

npm install --save vue@3.0.2

npm install --save vuex@4.0.0

解决版本冲突

可能是npm版本问题报错

解决方法:在命令后面加上

  --legacy-peer-deps

3.7 安装VueRouter

npm install vue-router --save-dev

vue-router是Vue.js官方的路由插件,它和vue.js是深度集成的,适合用于构建单页面应用。vue的单页面应用是基于路由和组件的,路由用于设定访问路径,并将路径和组件映射起来。

router文件夹下,有一个index.js,即为路由配置文件。可以设置多个路由,‘/index’,’/list’之类的,当然首先得引入该组件,再为该组件设置路由。

3.8 安装font-awesome

npm install font-awesome

四、前端拦截器

4.1 配置登陆拦截器

顾名思义就是对请求的拦截,请求接口之前或之后的预处理工作。分别为请求拦截器和响应拦截器, 执行顺序: 请求拦截器 -> api请求 -> 响应拦截器。 拦截器的作用:a. 统计api从发起请求到返回数据需要的时间;b. 配置公共的请求头,加载弹窗等;c. 对响应状态码做拦截,比入后端返回400或500的状态码, 返回对应错误信息。

4.2 axios请求拦截器request

在vue项目中,我们通常使用与后台进行数据交互,axios是一款基于promise封装的库,可以运行在浏览器端和node环境中。请求拦截器request作用:在请求发送前统一执行某些操作,常用在请求头中处理token等 

添加请求拦截器的方法

  1. axios.interceptors.request.use(function (config) {
  2. // 在发送请求之前做些什么
  3. return config;
  4. }, function (error) {
  5. // 对请求错误做些什么
  6. return Promise.reject(error);
  7. })

4.3 axios响应拦截器response

返回对象response中有response.status:Http响应码;response.data:后端返回的Json对象,包括response.data.code业务逻辑响应码,response.data.message:后端返回的响应提示信息;

添加响应拦截器方法

  1. axios.interceptors.response.use(function (response) {
  2. // 对响应数据做点什么
  3. return response;
  4. }, function (error) {
  5. // 对响应错误做点什么
  6. return Promise.reject(error);
  7. });
  8. }

4.4 封装请求

 在项目中,我们并不会直接使用 axios,而是会对它进行一层封装。 通过export导出封装的请求,如定义一个postRequest方法接收url和params,然后axios对象。在axios里进行实际接口调用操作。

  1. export const postRequest = (url, params) => {
  2. return axios({
  3. method: 'post',
  4. url: `${base}${url}`,
  5. data: params
  6. })
  7. }

4.5 代码实现src/utils/api.js

  1. import axios from "axios";
  2. import {Message} from "element-ui";
  3. import router from "@/router";
  4. // 请求拦截器
  5. axios.interceptors.request.use(config => {
  6. // 如果存在 token,请求携带这个 token( 登录的时候 把 token 存入了 sessionStorage )
  7. if (window.sessionStorage.getItem("tokenStr")) {
  8. // token 的key : Authorization ; value: tokenStr
  9. config.headers['Authorization'] = window.sessionStorage.getItem('tokenStr')
  10. }
  11. return config;
  12. },error => {
  13. console.log(error)
  14. })
  15. // 响应拦截器 - 统一处理消息提示
  16. axios.interceptors.response.use(success => {
  17. // 业务逻辑错误
  18. if (success.status && success.status === 200) { // 调到接口
  19. // 后端:500 业务逻辑错误,401 未登录,403 无权访问;
  20. if (success.data.code === 500 || success.data.code === 401 || success.data.code === 403) {
  21. Message.error({message: success.data.message})
  22. return
  23. }
  24. if (success.data.message) { // 输出后端 添加成功 之类的信息
  25. Message.success({message: success.data.message})
  26. }
  27. }
  28. return success.data
  29. }, error => { // 没访问到后端接口
  30. if (error.response.code === 504 || error.response.code === 404) {
  31. Message.error({message: '服务器不存在'})
  32. } else if (error.response.code === 403) {
  33. Message.error({message: '权限不足,请联系管理员!'})
  34. } else if (error.response.code === 401) {
  35. Message.error({message: '您还未登录,请登录!'})
  36. router.replace('/') // 路由替换
  37. } else {
  38. if (error.response.data.message) {
  39. Message.error({message: error.response.data.message})
  40. } else {
  41. Message.error({message: '未知错误!'})
  42. }
  43. }
  44. return
  45. })
  46. // 预备前置路径
  47. let base = '';
  48. // 传送 json 格式的 post 请求
  49. export const postRequest = (url, params) => {
  50. return axios({
  51. method: 'post',
  52. url: `${base}${url}`,
  53. data: params
  54. })
  55. }
  56. // 传送 json 格式的 get 请求
  57. export const getRequest = (url, params) => {
  58. return axios({
  59. method: 'get',
  60. url: `${base}${url}`,
  61. data: params
  62. })
  63. }
  64. // 传送 json 格式的 put 请求
  65. export const putRequest = (url, params) => {
  66. return axios({
  67. method: 'put',
  68. url: `${base}${url}`,
  69. data: params
  70. })
  71. }
  72. // 传送 json 格式的 delete 请求
  73. export const deleteRequest = (url, params) => {
  74. return axios({
  75. method: 'delete',
  76. url: `${base}${url}`,
  77. data: params
  78. })
  79. }


顾名思义就是对请求的拦截,分别为请求拦截器和响应拦截器, 执行顺序: 请求拦截器 -> api请求 -> 响应拦截器。 拦截器的作用:a. 统计api从发起请求到返回数据需要的时间;b. 配置公共的请求头,加载弹窗等;c. 对响应状态码做拦截,比入后端返回400或500的状态码, 返回对应错误信息。

4.6 main.js全局引入封装请求

通过main.js全局引入然后通过插件的方式使用方法。在具体调用时使用this.putRequest(url,params)形式使用

  1. import {postRequest} from "@/utils/api";
  2. import {putRequest} from "@/utils/api";
  3. import {getRequest} from "@/utils/api";
  4. import {deleteRequest} from "@/utils/api";
  1. Vue.prototype.postRequest = postRequest
  2. Vue.prototype.putRequest = putRequest
  3. Vue.prototype.getRequest = getRequest
  4. Vue.prototype.deleteRequest = deleteRequest

五、登陆页面

5.1 样式设计

为了设计界面,我们需要关注的地方是 <template> 标签内的 html 和 <style> 标签内的 css。登录框我们一般会用 Form 来做,打开 Element 的组件文档(),发现它为我们提供了丰富的 Form 组件,我们可以点击“显示代码”,复制我们需要的部分。

不过这里好像并没有特别符合我们应用场景的表单,或者说这些都是比较复杂的,我们只需要其中的一小部分。把页面再往下拉,可以看到关于这个组件的属性、事件、方法等的文档,根据这个文档,我们可以自己去构建需要的表单。

5.2 登陆页功能设计

5.3 Login.vue登录页

验证码通过后端返回图片。表单通过rules绑定规则,通过prop为元素添加属性,在rules里写规则。验证方式:this.$refs.loginForm.validate。

/captcha返回信息

 /login登陆返回报文

登陆成功后会返回一个token。此token会作为后面前后端数据交互的一个凭证。为了保证系统安全性后端会定期更新该token,超过token的失效之后用户需要重新登录。前端将获取来的token存入sessionStorage中作为之后调用接口的钥匙,此后通过Axios进行get或者post请求时都需要带上此token。

在请求拦截器里判断toke是否存在,之后每次请求都会校验token,如果存在则请求携带token,放入Authorization参数中;后端校验token。

前端登陆成功后 通过this.$router.replace('/home') 跳转到home首页。replace方法替换后点击浏览器回退按钮不会跳转到登陆页面。登陆失败后端返回失败原因。

在用户未登陆的情况下,如果用户不是以访问登陆页,而是访问某个登陆后才能访问的路由,如。需要分情况讨论:1.用户可能输入首页地址或错误地址,登陆成功后让他跳到首页;2.否则成功跳转到他输入的地址。

this.$router.replace((path === '/' || path === undefined) ? '/home' : path)

  1. <template>
  2. <div>
  3. <el-form
  4. v-loading="loading"
  5. element-loading-text="正在登录......"
  6. element-loading-spinner="el-icon-loading"
  7. element-loading-background="rgba(0, 0, 0, 0.8)"
  8. ref="loginForm" :model="loginForm" :rules="rules" class="loginContainer">
  9. <h3 class="loginTitle">系统登录</h3>
  10. <el-form-item prop="username">
  11. <el-input type="text" v-model="loginForm.username" placeholder="请输入用户名"></el-input>
  12. </el-form-item>
  13. <el-form-item prop="password">
  14. <el-input type="password" v-model="loginForm.password" placeholder="请输入密码"></el-input>
  15. </el-form-item>
  16. <el-form-item prop="code">
  17. <el-input type="text" v-model="loginForm.code" placeholder="点击图片更换验证码"
  18. style="width: 250px;margin-right: 5px;"></el-input>
  19. <img :src="captchaUrl" @click="updateCaptcha">
  20. </el-form-item>
  21. <el-button type="primary" style="width: 100%" @click="submitLogin">登录</el-button>
  22. </el-form>
  23. </div>
  24. </template>
  25. <script>
  26. export default {
  27. name: 'Login',
  28. components: {},
  29. props: [],
  30. data() {
  31. return {
  32. // 验证码
  33. captchaUrl:'/captcha?time=' + new Date(),//获取响应码后端接口
  34. loginForm: {
  35. username: 'admin',
  36. password: '123',
  37. code: '',
  38. },
  39. loading: false, // 加载中
  40. //校验规则,与表单绑定
  41. rules: {
  42. username: [{required: true, message: '请输入用户名', trigger: 'blur'}],
  43. password: [{required: true, message: '请输入密码', trigger: 'blur'}],
  44. code: [{required: true, message: '请输入验证码', trigger: 'blur'}]
  45. }
  46. }
  47. },
  48. mounted(){
  49. },
  50. methods: {
  51. // 点击刷新验证码
  52. updateCaptcha() {
  53. this.captchaUrl="/captcha?time="+new Date();
  54. },
  55. submitLogin() {
  56. // 登录
  57. this.$refs.loginForm.validate((valid) => {
  58. if (valid) {
  59. this.loading = true;//准备调登录接口时,出现正在加载
  60. //第一个参数请求后端的地址,第二个参数,传给后端的数据
  61. this.postRequest('/login', this.loginForm).then(resp => {
  62. this.loading = false;//登录成功后关闭
  63. if (resp) {
  64. // 存储用户 token 到 sessionStorage
  65. const tokenStr = resp.obj.tokenHead + resp.obj.token;
  66. window.sessionStorage.setItem('tokenStr', tokenStr);
  67. // 跳转到首页
  68. // this.$router.push('/home') // 路由跳转,可以回退到上一页
  69. this.$router.replace('/home') // 路径替换,无法回退到上一页
  70. // 页面跳转
  71. // 拿到用户要跳转的路径
  72. let path = this.$route.query.redirect;
  73. // 用户可能输入首页地址或错误地址,让他跳到首页,否则跳转到他输入的地址
  74. this.$router.replace((path === '/' || path === undefined) ? '/home' : path)
  75. }
  76. })
  77. } else {
  78. this.$message.error('请输入所有字段!');
  79. return false;
  80. }
  81. })
  82. }
  83. }
  84. }
  85. </script>
  86. <style>
  87. .loginContainer {
  88. border-radius: 15px;
  89. background-clip: padding-box;
  90. /*属性规定背景的绘制区域 背景被裁剪到内边距框。 margin: 180 px auto;*/
  91. margin: 180px auto;
  92. width: 350px;
  93. padding: 15px 35px;
  94. background: #fff;
  95. border: 1px solid #eaeaea;
  96. box-shadow: 0 0 25px #cac6c6;
  97. /* X轴偏移量 Y轴偏移量 [阴影模糊半径] [阴影扩展] [阴影颜色] [投影方式]; */
  98. }
  99. .loginTitle {
  100. margin: 0 auto 40px auto;
  101. text-align: center;
  102. }
  103. .loginRemember {
  104. text-align: left;
  105. margin: 0 0 15px 0;
  106. }
  107. /*验证码*/
  108. .el-form-item__content {
  109. display: flex;
  110. align-items: center;
  111. }
  112. </style>

 SessionStorage.setItem()中,为了使axios做下一次请求时获取token认证,登录之后拿到token放到sessionStrorage里         

    // 存储用户 token 到 sessionStorage

                const tokenStr = resp.obj.tokenHead + resp.obj.token;

                window.sessionStorage.setItem('tokenStr', tokenStr);

5.4 配置页面路由——router/index.js

  1. import Vue from 'vue'
  2. import Router from 'vue-router'
  3. import Login from "@/views/Login";
  4. Vue.use(Router)
  5. export default new Router({
  6. routes: [
  7. {
  8. path: '/',
  9. name: 'Login',
  10. component: Login,
  11. hidden: true // 不会被循环遍历出来
  12. },
  13. ]
  14. })

5.5 前端路由导航守卫

登录页面的开发似乎已经较为完善了,但其实还没有完,因为这个登录页面其实没有用,别人直接输入首页的网址,就可以绕过登录页面。为了让它发挥作用,我们还需要开发一个拦截器。使用钩子函数判断是否拦截函数及在某些时机会被调用的函数。这里我们使用 router.beforeEach(),意思是在访问每一个路由前调用。to 要去的路由; from 来自哪里的路由 ; next() 放行。

通过sessionStorage.getItem('user')获取用户的token,如果token不存在则需要登陆。

在判断是否为if (to.path == '/')登陆页,是的话放行,否则按用户指定的路由登陆;

main.js

  1. // 使用 router.beforeEach 注册一个全局前置守卫
  2. router.beforeEach((to, from, next) => {
  3. // to 要去的路由; from 来自哪里的路由 ; next() 放行
  4. // 用户登录成功时,把 token 存入 sessionStorage,如果携带 token,初始化菜单,放行
  5. if (window.sessionStorage.getItem('tokenStr')) {
  6. // 如果用户不存在
  7. //待首页功能部分完善后补充
  8. } else {
  9. if (to.path === '/') {
  10. next()
  11. } else {
  12. next('/?redirect=' + to.path)
  13. }
  14. }
  15. })

5.6 解决前后端跨域

前端端口默认8080,假设后端端口是8081,那8080如何访问到8081的数据,我们通过Node.js实现端口自动转发。浏览器的同源策略:两个页面必须具有相同的协议(protocol)主机(host)端口号(port)。同源策略是浏览器的一种安全机制,它是指浏览器会阻止对非同源页面的DOM操作以及XMLHttpRequest对象向非同源服务器发起http请求。请求一个接口时,出现Access-Control-Allow-Origin等,说明出现请求跨域了。vue中解决跨域的方法:配置vue.config.js文件,如果没有就自行新建一个。

原理:

1.将域名发送给本地的服务器(localhost:8080)

2.再由本地的服务器去请求真正的服务器

3.因为请求是从服务端发出的,所以不存在跨域的问题了。

在vue中是由node.js自动进行的

前端反向代理

vue.config.js

修改proxyTable 请求地址经过node.js后代理到后端地址8081

  1. proxyTable: {
  2. '/': {
  3. changeOrigin: true, //跨域
  4. target: 'http://localhost:8081',
  5. pathRewrite: {
  6. // '^/api': ''
  7. }
  8. },
  9. },

5.7 运行项目

六、首页页面  

我们的项目虽然本质上是单页面应用,但表面上有多个功能页面。为了方便用户在这各个页面之间切换,我们需要添加一个导航栏。这个导航栏的要求很简单:

能够在每个页面显示

美观

为了实现第一个要求,我们需要把导航栏放在其它页面的父页面中(对 Vue 来说就是父组件),之前我们讲过,App.vue 是所有组件的父组件,但把导航栏放进去不合适,因为我们的登录页面中不应该显示导航栏。为了解决这个问题,我们在views目录下直接新建一个组件,命名为 Home.vue。和 App.vue 一样,写入了一个

<router-view/>,也就是子页面(组件)显示的地方。

Home.vue整体上实现了首页左侧菜单的获取和展示,右上角的个人中心的设置。从store.state中获取当前菜单信息、当前用户的登陆信息.。

 6.1 菜单功能设计与实现

需要文件目录如下:views/emp基本资料    新建 EmpBasic.vue  EmpAdv.vue

views/per  员工资料新建 PerEmp.vu  PerEc.vue  PerTrain.vue  PerSalary.vue   PerMv.vue

views/sal  工资账套 SalSob.vue SalSobcfg.vue SalTable.vue SalMonth.vue  SalSearch.vue

views/sta 综合信息统计 新增StaAll.vue  StaScore.vue   StaPers.vue  StaRecord.vue

views/sys 系统管理  新增 SysBasic.vue SysConfig.vue SysLog.vue  SysAdmin.vue  SysData.vue SysInit.vue

配置store/index.js

通过vuex进行路由状态管理

  1. import Vue from 'vue'
  2. import Vuex from 'vuex'
  3. Vue.use(Vuex)
  4. // 导入 Vuex
  5. const store = new Vuex.Store({
  6. state: {
  7. routes: []
  8. },
  9. mutations: { // 与 state 同步执行;可以改变 state 对应的值的方法
  10. // 初始化路由 菜单
  11. initRoutes(state, data) {
  12. state.routes = data
  13. },
  14. },
  15. // 异步执行
  16. actions: {
  17. }
  18. })
  19. export default store;

在Main.js中引入store

  1. import store from './store'
  2. new Vue({
  3. router,
  4. store,
  5. render: h => h(App)
  6. }).$mount('#app')

6.2 封装菜单请求工具

后端请求菜单接口返回信息

我们设计的菜单是根据用户信息加载的路由信息,即不同用户可能有不同的菜单权限。接口返回的菜单信息如下。通过children表示子菜单,子菜单中的parentId与父菜单的id相等时表示一个确定的父子菜单关系。如下的关系表示有一个层级菜单“员工资料/基本资料”。

如果store.state.routes有数据,初始化路由菜单。通过getRequest('/system/config/menu')方法从后端获取路由数据,按照层次关系拆分。

如何根据接口中的component字段找到对应的代码路径呢?

通过对接口对象中的component字段分类查找,例如component以Home开头,源代码在src/views/Home.vue中。

  1. if (component.startsWith('Home')) {
  2. require(['@/views/' + component + '.vue'], resolve);
  3. }

initMenu方法将路由数据存于store中,如果store中有数据则无需初始化,否则,初始化。

什么时候调用?每一个页面都需要调用初始化菜单方法。放在路由拦截器里,每次访问路由都要执行一次。

  1. import {getRequest} from "@/utils/api";
  2. // 菜单请求工具类
  3. // router 路由; store Vuex
  4. export const initMenu = (router, store) => {
  5. // 如果有数据,初始化路由菜单
  6. if (store.state.routes.length > 0) {
  7. return;
  8. }
  9. getRequest('/system/config/menu').then(data => {
  10. // 如果数据存在 格式化路由
  11. if (data) {
  12. // 格式化好路由
  13. let fmtRoutes = formatRoutes(data)
  14. // 添加到 router
  15. router.addRoutes(fmtRoutes)
  16. // 将数据存入 Vuex
  17. store.commit('initRoutes',fmtRoutes)
  18. // 连接 WebSocket
  19. store.dispatch('connect')
  20. }
  21. })
  22. }
  23. export const formatRoutes = (routes) => {
  24. let fmtRoutes = []
  25. routes.forEach(router => {
  26. let {
  27. path,
  28. component,
  29. name,
  30. iconCls,
  31. children
  32. } = router;
  33. // 如果有 children 并且类型是数组
  34. if (children && children instanceof Array) {
  35. // 递归
  36. children = formatRoutes(children)
  37. }
  38. // 单独对某一个路由格式化 component
  39. let fmRouter = {
  40. path: path,
  41. name: name,
  42. iconCls: iconCls,
  43. children: children,
  44. component(resolve) {
  45. // 判断组件以什么开头,到对应的目录去找
  46. if (component.startsWith('Home')) {
  47. require(['@/views/' + component + '.vue'], resolve);
  48. }else if (component.startsWith('Emp')) {
  49. require(['@/views/emp/' + component + '.vue'], resolve);
  50. }else if (component.startsWith('Per')) {
  51. require(['@/views/per/' + component + '.vue'], resolve);
  52. }else if (component.startsWith('Sal')) {
  53. require(['@/views/sal/' + component + '.vue'], resolve);
  54. }else if (component.startsWith('Sta')) {
  55. require(['@/views/sta/' + component + '.vue'], resolve);
  56. }else if (component.startsWith('Sys')) {
  57. require(['@/views/sys/' + component + '.vue'], resolve);
  58. }
  59. }
  60. }
  61. fmtRoutes.push(fmRouter)
  62. })
  63. return fmtRoutes
  64. }

更新main.js

获取当前用户登陆信息

将当前用户信息保存在sessionStorage的user中,每次路由切换时获取用户的登陆信息。

  1. // 使用 router.beforeEach 注册一个全局前置守卫
  2. router.beforeEach((to, from, next) => {
  3. // to 要去的路由; from 来自哪里的路由 ; next() 放行
  4. // 用户登录成功时,把 token 存入 sessionStorage,如果携带 token,初始化菜单,放行
  5. if (window.sessionStorage.getItem('tokenStr')) {
  6. initMenu(router, store)
  7. // 如果用户不存在
  8. if (!window.sessionStorage.getItem('user')
  9. ) {
  10. // 判断用户信息是否存在
  11. return getRequest('/admin/info').then(resp => {
  12. if (resp) {
  13. // 存入用户信息,转字符串,存入 sessionStorage
  14. window.sessionStorage.setItem('user', JSON.stringify(resp))
  15. // 同步用户信息 编辑用户
  16. store.commit('INIT_ADMIN',resp)
  17. next();
  18. }
  19. })
  20. }
  21. next();
  22. } else {
  23. if (to.path === '/') {
  24. next()
  25. } else {
  26. next('/?redirect=' + to.path)
  27. }
  28. }
  29. })

6.3 样式设计

登陆后的前端页面被分解为上方导航栏,左侧菜单栏和中间的主要功能区域,对于不同页面的切换,仅需要变化中间功能区域内容,提高了代码重用性。首先自定义页面各区域组件并将各组件文件保存到Views文件夹中,每个.vue文件都是单独的组件,路由中指定的组件通过导入语句<router-view></router-view>在页面中渲染。

布局使用了element-ui的container布局容器:el-container 外层容器;el-header 顶栏容器;el-aside 侧边栏容器 ;el-menu导航区域;el-main 主要区域容器;el-footer底栏容器

样式设计如下:

在el-menu导航里添加router属性实现菜单路由的动态渲染;首页导航菜单使用element-ui的NavMenu导航菜单控件。使用属性unique-opened:保证每次点击菜单只有一个菜单的展开。使用router属性,在激活导航时以 index 作为 path 进行路由跳转。

通过el-dropdown的@command点击菜单项触发的事件回调方法绑定el-dropdown-item中的command,实现注销登陆和进入个人中心功能。

elemet的MessageBox弹框实现注销登陆提示弹框。退出登陆后清除vuex中的菜单信息。

使用el-breadcrumb面包屑控件实现显示当前页面的路径,快速返回之前的任意页面功能。对于非首页的页面v-if="this.$router.currentRoute.path!=='/home'"显示层级:首先/当前页。

对于首页v-if="this.$router.currentRoute.path==='/home'",显示欢迎字体。

6.4 Home.vue代码

  1. <template>
  2. <div>
  3. <el-container>
  4. <el-header class="homeHeader">
  5. <div class="title">云办公</div>
  6. <!-- 1-1 添加在线聊天入口 -->
  7. <div>
  8. <el-button type="text" icon="el-icon-bell" size="normal"
  9. style="margin-right: 8px;color: black;" @click="goChar"></el-button>
  10. <el-dropdown class="userInfo" @command="commandHandler">
  11. <span class="el-dropdown-link">
  12. {{ user.name }}<i><img :src="user.userFace"></i>
  13. </span>
  14. <el-dropdown-menu slot="dropdown">
  15. <el-dropdown-item command="userinfo">个人中心</el-dropdown-item>
  16. <el-dropdown-item command="setting">设置</el-dropdown-item>
  17. <el-dropdown-item command="logout">注销登录</el-dropdown-item>
  18. </el-dropdown-menu>
  19. </el-dropdown>
  20. </div>
  21. </el-header>
  22. <el-container>
  23. <el-aside width="200px">
  24. <!-- 1、添加 router -->
  25. <el-menu router unique-opened>
  26. <!-- 2、循环整个路由组件,不展示 hidden: true 的路由组件 -->
  27. <el-submenu :index="index +''" v-for="(item,index) in routes"
  28. :key="index" v-if="!item.hidden">
  29. <template slot="title"><i :class="item.iconCls" style="color: black;margin-right: 5px"></i>
  30. <span>{{ item.name }}</span>
  31. </template>
  32. <!-- 3、循环遍历子路由 -->
  33. <el-menu-item :index="children.path"
  34. v-for="(children,index) in item.children" :key="index">{{ children.name }}
  35. </el-menu-item>
  36. </el-submenu>
  37. </el-menu>
  38. </el-aside>
  39. <el-main>
  40. <!-- 面包屑导航区域 -->
  41. <el-breadcrumb separator-class="el-icon-arrow-right"
  42. v-if="this.$router.currentRoute.path!=='/home'">
  43. <el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
  44. <el-breadcrumb-item>{{ this.$router.currentRoute.name }}</el-breadcrumb-item>
  45. </el-breadcrumb>
  46. <div class="homeWelcome" v-if="this.$router.currentRoute.path==='/home'">
  47. 欢迎来到云办公系统!
  48. </div>
  49. <!-- 路由点位符 -->
  50. <router-view class="homeRouterView"/>
  51. </el-main>
  52. </el-container>
  53. </el-container>
  54. </div>
  55. </template>
  56. <script>
  57. export default {
  58. name: 'Home',
  59. data() {
  60. return {
  61. // 获取用户信息,将字符串转对象
  62. // user: JSON.parse(window.sessionStorage.getItem('user'))
  63. }
  64. },
  65. computed: {
  66. // 从 vuex 获取 routes
  67. routes() {
  68. return this.$store.state.routes
  69. },
  70. user() {
  71. return this.$store.state.currentAdmin
  72. }
  73. },
  74. methods: {
  75. // 1-2 进入在线聊天页面
  76. goChar() {
  77. this.$router.push('/chat')
  78. },
  79. // 注销登录
  80. commandHandler(command) {
  81. if (command === 'logout') {
  82. // 弹框提示用户是否要删除
  83. this.$confirm('此操作将注销登录, 是否继续?', '提示', {
  84. confirmButtonText: '确定',
  85. cancelButtonText: '取消',
  86. type: 'warning'
  87. }).then(() => {
  88. // 注销登录
  89. this.postRequest('/logout')
  90. // 清空用户信息
  91. window.sessionStorage.removeItem('tokenStr')
  92. window.sessionStorage.removeItem('user')
  93. // 路由替换到登录页面
  94. // this.$router.replace('/')
  95. // 清空菜单信息;在src/utils/menus.js 中初始化菜单信息
  96. this.$store.commit('initRoutes', [])
  97. this.$router.replace('/')
  98. }).catch(() => {
  99. this.$message({
  100. type: 'info',
  101. message: '已取消注销登录'
  102. });
  103. });
  104. }
  105. if (command === 'userinfo') {
  106. this.$router.push('/userinfo')
  107. }
  108. }
  109. }
  110. }
  111. </script>
  112. <style scoped>
  113. .homeHeader {
  114. background: #3e9ef5;
  115. display: flex;
  116. align-items: center;
  117. justify-content: space-between;
  118. padding: 0 15px;
  119. box-sizing: border-box;
  120. }
  121. .homeHeader .title {
  122. font-size: 30px;
  123. /*font-family: 微软雅黑;*/
  124. font-family: 华文楷体;
  125. color: white;
  126. }
  127. .homeHeader .userInfo {
  128. cursor: pointer;
  129. }
  130. .el-dropdown-link img {
  131. width: 48px;
  132. height: 48px;
  133. border-radius: 50%;
  134. margin-left: 8px;
  135. }
  136. .homeWelcome {
  137. text-align: center;
  138. font-size: 30px;
  139. font-family: 华文楷体;
  140. color: #409ef4;
  141. padding-top: 50px;
  142. }
  143. .homeRouterView {
  144. margin-top: 10px;
  145. }
  146. </style>

忽略hidden属性

            <!-- 2、循环整个路由组件,不展示 hidden: true 的路由组件 -->

            <el-submenu :index="index +''" v-for="(item,index) in routes"

                        :key="index" v-if="!item.hidden">

6.5 更新路由router/index.js

忽略router/index.js的hidden:true的

/home路由从首页获取

  1. import Vue from 'vue'
  2. import VueRouter from 'vue-router'
  3. import Login from "@/views/Login";
  4. Vue.use(VueRouter)
  5. const routes = [
  6. {
  7. path: '/',
  8. name: 'Login',
  9. component: Login,
  10. hidden: true // 不会被循环遍历出来
  11. }
  12. ]
  13. const router = new VueRouter({
  14. routes
  15. })
  16. export default router

6.6 index.html消除边距

添加样式

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width,initial-scale=1.0">
  6. <title>yeb-front</title>
  7. </head>
  8. <body style="margin:0px;padding:0px">
  9. <div id="app"></div>
  10. <!-- built files will be auto injected -->
  11. </body>
  12. </html>

七、基础信息设置

7.1 样式设计

系统管理/基础信息设置设计如下几个模块:部门管理、职位管理、职称管理、奖惩规则、权限组

Tabs 标签页

使用element的Tabs标签页完成不同业务功能的切换;分隔内容上有关联但属于不同类别的数据集合。

abs 组件提供了选项卡功能,默认选中第一个标签页,你也可以通过 value 属性来指定当前选中的标签页。

  1. <template>
  2. <el-tabs v-model="activeName" @tab-click="handleClick">
  3. <el-tab-pane label="用户管理" name="first">用户管理</el-tab-pane>
  4. <el-tab-pane label="配置管理" name="second">配置管理</el-tab-pane>
  5. <el-tab-pane label="角色管理" name="third">角色管理</el-tab-pane>
  6. <el-tab-pane label="定时任务补偿" name="fourth">定时任务补偿</el-tab-pane>
  7. </el-tabs>
  8. </template>
  9. <script>
  10. export default {
  11. data() {
  12. return {
  13. activeName: 'second'
  14. };
  15. },
  16. methods: {
  17. handleClick(tab, event) {
  18. console.log(tab, event);
  19. }
  20. }
  21. };
  22. </script>

7.2 组件化开发

在我们开发的过程中会遇到很多可以重复使用的代码块,而Vue则提供了这样的封装方式也就是Vue.。利用组件化开发,将部门管理、职位管理、职称管理、奖惩规则、权限组等使用组件方式引入。组件也是.vue文件。组件导入方式 import 组件名 from “组件路径”,示例:

import DepMana from "@/components/sys/basic/DepMana";导入后并不能直接使用,需要在components对象中注册组件。之后是组件的应用:<DepMana/>或<DepMana></DepMana/>

组件目录

7.3 SysBasic.vue

  1. <template>
  2. <div>
  3. <el-tabs v-model="activeName" type="card">
  4. <el-tab-pane label="部门管理" name="DepMana"><DepMana/></el-tab-pane>
  5. <el-tab-pane label="职位管理" name="PosMana"><PosMana/></el-tab-pane>
  6. <el-tab-pane label="职称管理" name="JobLevelMana"><JobLevelMana/></el-tab-pane>
  7. <el-tab-pane label="奖惩规则" name="EcMana"><EcMana/></el-tab-pane>
  8. <el-tab-pane label="权限组" name="PositionMana"><PositionMana/></el-tab-pane>
  9. </el-tabs>
  10. </div>
  11. </template>
  12. <script>
  13. import DepMana from "@/components/sys/basic/DepMana"; // 部门管理
  14. import EcMana from "@/components/sys/basic/EcMana"; // 奖惩规则
  15. import JobLevelMana from "@/components/sys/basic/JobLevelMana"; // 职称管理
  16. import PositionMana from "@/components/sys/basic/PositionMana"; // 权限组
  17. import PosMana from "@/components/sys/basic/PosMana"; // 职位管理
  18. export default {
  19. name: "SysBasic",
  20. components:{
  21. JobLevelMana,
  22. DepMana,
  23. EcMana,
  24. PositionMana,
  25. PosMana
  26. },
  27. data() {
  28. return {
  29. activeName: 'DepMana' // 激活项
  30. }
  31. },
  32. methods: {}
  33. }
  34. </script>
  35. <style scoped>
  36. </style>

7.4 部门管理DepMana.vue组件

为了使用方便,一次性加载所有的部门。

样式设计:flex布局,space-between:两端对齐,与父元素对齐宽度。

搜索框过滤节点的方法,通过filterNode方法传入两个参数,value-搜索输入的值,data树形标签绑定的数据。value为空时展示所有标签值,否则,判断输入的value能否在data.name(展示在树形标签上的值)找到,这里用了js查找字符串方法indexOf,返回查找元素的下标,能找到返回下标(>=0)

 expand-on-click-node仅当鼠标点击展开按钮时展开,方便添加和删除功能的使用。

点击按钮时获取当前节点的数据即要添加子部门的“上级部门”id(parentId)

  1. <template>
  2. <div style="width: 500px">
  3. <!-- 1 -->
  4. <el-input
  5. placeholder="请输入部门名称进行搜索..."
  6. prefix-icon="el-icon-search"
  7. v-model="filterText">
  8. </el-input>
  9. <!-- 9、:expand-on-click-node="false" 点击小三角箭头才会展开
  10. :default-expand-all="false" 设置默认不展开所有节点 -->
  11. <el-tree
  12. :data="deps"
  13. :props="defaultProps"
  14. default-expand-all
  15. :filter-node-method="filterNode"
  16. :expand-on-click-node="false"
  17. ref="tree">
  18. <!-- 7、label: 'name' -->
  19. <!-- 8、style="display: flex;justify-content: space-between;width: 100% 父容器宽度" 让添加和删除按键居右 -->
  20. <span class="custom-tree-node" slot-scope="{ node, data }"
  21. style="display: flex;justify-content: space-between;width: 100%">
  22. <span>{{ data.name }}</span>
  23. <span>
  24. <el-button
  25. plain
  26. type="primary"
  27. size="mini"
  28. class="depBtn"
  29. @click="() => showAddDep(data)">
  30. 添加部门
  31. </el-button>
  32. <!-- 10、showAddDep(data) deleteDep(data) data 后端传过来的完整的 json 对象 -->
  33. <el-button
  34. plain
  35. type="danger"
  36. size="mini"
  37. class="depBtn"
  38. @click="() => deleteDep(data)">
  39. 删除部门
  40. </el-button>
  41. </span>
  42. </span>
  43. </el-tree>
  44. <!-- 13、对话弹框 -->
  45. <el-dialog
  46. title="添加部门"
  47. :visible.sync="dialogVisible"
  48. width="30%">
  49. <!-- 16 -->
  50. <div>
  51. <table>
  52. <tr>
  53. <td>
  54. <el-tag>上级部门</el-tag>
  55. </td>
  56. <td>{{ pname }}</td>
  57. </tr>
  58. <tr>
  59. <td>
  60. <el-tag>部门名称</el-tag>
  61. </td>
  62. <td>
  63. <el-input v-model="dep.name" placeholder="请输入部门名称..." size="small"></el-input>
  64. </td>
  65. </tr>
  66. </table>
  67. </div>
  68. <span slot="footer" class="dialog-footer">
  69. <el-button @click="dialogVisible = false">取 消</el-button>
  70. <!-- 18、确定添加按钮绑定事件 @click="doAddDep" -->
  71. <el-button type="primary" @click="doAddDep">确 定</el-button>
  72. </span>
  73. </el-dialog>
  74. </div>
  75. </template>
  76. <script>
  77. export default {
  78. name: "DepMana",
  79. data() {
  80. return { // 2
  81. filterText: '',
  82. deps: [], // 所有部门整个数组
  83. defaultProps: { // 2 关联子部门
  84. children: 'children',
  85. label: 'name'
  86. },
  87. dialogVisible: false, // 14
  88. dep: { // 15、添加部门数据象
  89. name: '',
  90. parentId: -1,
  91. isParent: ''
  92. },
  93. pname: '' // 15、上级部门名称
  94. }
  95. },
  96. watch: {
  97. // 4、观察者事件,监控输入框的值(框架方法)
  98. filterText(val) {
  99. this.$refs.tree.filter(val);
  100. }
  101. },
  102. mounted() {
  103. this.initDeps() // 6、调用获取所有部门方法
  104. },
  105. methods: {
  106. // 删除部门调用的方法
  107. removeDepFromDeps(p, deps, id) {
  108. for (let i = 0; i < deps.length; i++) {
  109. let d = deps[i]
  110. if (d.id === id) {
  111. deps.splice(i, 1)
  112. if (deps.length === 0) {
  113. p.isParent = false
  114. }
  115. return;
  116. } else {
  117. this.removeDepFromDeps(d, d.children, id)
  118. }
  119. }
  120. },
  121. // 12、删除部门
  122. deleteDep(data) {
  123. // console.log(data)
  124. if (data.isParent) {
  125. this.$message.error('父部门删除失败!')
  126. } else {
  127. this.$confirm('此操作将永久删除该[' + data.name + ']部门, 是否继续?', '提示', {
  128. confirmButtonText: '确定',
  129. cancelButtonText: '取消',
  130. type: 'warning'
  131. }).then(() => {
  132. this.deleteRequest('/system/basic/department/' + data.id).then(resp => {
  133. if (resp) {
  134. this.removeDepFromDeps(null, this.deps, data.id)
  135. }
  136. })
  137. }).catch(() => {
  138. this.$message({
  139. type: 'info',
  140. message: '已取消删除'
  141. });
  142. });
  143. }
  144. },
  145. // 20、添加完部门 初始化 清空数据
  146. initDep() {
  147. this.dep = {
  148. name: '',
  149. parentId: -1
  150. }
  151. this.pname = ''
  152. },
  153. // 22、 递归查询所有部门信息,deps 查询到的整个数组,dep 添加的部门
  154. addDep2Deps(deps, dep) {
  155. for (let i = 0; i < deps.length; i++) {
  156. let d = deps[i] // 父部门
  157. if (d.id === dep.parentId) {
  158. d.children = d.children.concat(dep) // 把 dep 加为 d 的子部门
  159. if (d.children.length > 0) {
  160. d.isParent = true
  161. }
  162. return;
  163. } else {
  164. this.addDep2Deps(d.children, dep) // 递归调用此方法 以查询结果为条件 继续查询子部门
  165. }
  166. }
  167. },
  168. // 19、确认添加部门
  169. doAddDep() {
  170. this.postRequest('/system/basic/department/', this.dep).then(resp => {
  171. if (resp) {
  172. // console.log(resp)
  173. this.dialogVisible = false // 关闭对话框
  174. this.addDep2Deps(this.deps, resp.data) // 23、【无效】手动插入部门 显示添加后的数据
  175. this.initDep() // 21、调用初始化方法 清空数据
  176. }
  177. })
  178. },
  179. // 11、17、添加部门弹框
  180. showAddDep(data) {
  181. // console.log(data)
  182. this.dep.parentId = data.id
  183. this.pname = data.name
  184. this.dialogVisible = true
  185. },
  186. // 5、获取所有部门
  187. initDeps() {
  188. this.getRequest('/system/basic/department/').then(resp => {
  189. if (resp) {
  190. this.deps = resp
  191. }
  192. })
  193. },
  194. // 3、事件(框架方法)
  195. filterNode(value, data) { // data 整行数据
  196. if (!value) return true; // true 节点可以展示,false 节点隐藏
  197. return data.name.indexOf(value) !== -1; // label: 'name'
  198. }
  199. }
  200. }
  201. </script>
  202. <style scoped>
  203. /* 8 */
  204. .depBtn {
  205. padding: 2px;
  206. }
  207. </style>

7.5 职位管理PosMana.vue组件

element的表格控件,第一列做多选框,将其type="selection"。表格绑定多选事件@selection-change="handleSelectionChange"

 通过el-dialog编辑按钮修改职位名称。绑定弹出框显示方法:visible.sync="dialogVisible",取消时dialogVisible=false,点击“编辑”时dialogVisible=true。

 批量删除。通过multipleSelection数组获取多选数据,multipleSelection为空时,批量删除按钮禁用。提示框内通过multipleSelection展示多选信息

接口数据返回信息

 PosMana.vue

键盘事件@keydown.enter.native="addPosition"输入后自动调用”添加“按钮绑定的方法。

编辑职位信息后:刷新列表数据,关闭弹框。

使用数据的拷贝Object.assign(this.updatePos, data),将data中数据赋值给updatePos,避免浅拷贝引发的updatePos对data的数据修改。

  1. <template>
  2. <div>
  3. <div>
  4. <el-input
  5. size="small"
  6. class="addPosInput"
  7. placeholder="请选择日期"
  8. suffix-icon="el-icon-plus"
  9. @keydown.enter.native="addPosition"
  10. v-model="pos.name">
  11. </el-input>
  12. <el-button size="small" icon="el-icon-plus" type="primary" @click="addPosition">添加</el-button>
  13. </div>
  14. <div class="posManaMain">
  15. <el-table
  16. border
  17. stripe
  18. size="small"
  19. :data="positions"
  20. style="width: 70%"
  21. @selection-change="handleSelectionChange">
  22. <el-table-column
  23. type="selection"
  24. width="55">
  25. </el-table-column>
  26. <el-table-column
  27. prop="id"
  28. label="编号"
  29. width="55">
  30. </el-table-column>
  31. <el-table-column
  32. prop="name"
  33. label="职位"
  34. width="120">
  35. </el-table-column>
  36. <el-table-column
  37. prop="createDate"
  38. label="创建时间"
  39. width="200">
  40. </el-table-column>
  41. <el-table-column label="操作">
  42. <template slot-scope="scope">
  43. <el-button
  44. size="mini"
  45. @click="showEditView(scope.$index, scope.row)">编辑
  46. </el-button>
  47. <el-button
  48. size="mini"
  49. type="danger"
  50. @click="handleDelete(scope.$index, scope.row)">删除
  51. </el-button>
  52. </template>
  53. </el-table-column>
  54. </el-table>
  55. </div>
  56. <!-- :disabled 不禁用条件为勾选中,没勾选中为禁用。 -->
  57. <el-button size="small" style="margin-top: 8px" type="danger"
  58. :disabled="this.multipleSelection.length===0" @click="deleteMany">批量删除
  59. </el-button>
  60. <el-dialog
  61. title="提示"
  62. :visible.sync="dialogVisible"
  63. width="30%">
  64. <div>
  65. <el-tag>职位名称</el-tag>
  66. <el-input v-model="updatePos.name" size="small" class="updatePosInput"></el-input>
  67. </div>
  68. <span slot="footer" class="dialog-footer">
  69. <el-button size="small" @click="dialogVisible = false">取 消</el-button>
  70. <el-button size="small" type="primary" @click="doUpdate">确 定</el-button>
  71. </span>
  72. </el-dialog>
  73. </div>
  74. </template>
  75. <script>
  76. export default {
  77. name: "PosMana",
  78. data() {
  79. return {
  80. pos: { // 查询添加职位数据
  81. name: ''
  82. },
  83. positions: [],
  84. dialogVisible: false,
  85. updatePos: { // 更新职位数据
  86. name: ''
  87. },
  88. multipleSelection: [] // 批量删除勾选的对象
  89. }
  90. },
  91. mounted() {
  92. // 调用获取后端接口所有职位数据方法
  93. this.initPositions()
  94. },
  95. methods: {
  96. // 批量删除请求
  97. deleteMany() {
  98. this.$confirm('此操作将永久删除[' + this.multipleSelection.length + ']条职位, 是否继续?', '提示', {
  99. confirmButtonText: '确定',
  100. cancelButtonText: '取消',
  101. type: 'warning'
  102. }).then(() => {
  103. let ids = '?'
  104. this.multipleSelection.forEach(item => {
  105. ids += 'ids=' + item.id + '&'
  106. })
  107. this.deleteRequest('/system/basic/pos/' + ids).then(resp => {
  108. if (resp) {
  109. this.initPositions()
  110. }
  111. })
  112. }).catch(() => {
  113. this.$message({
  114. type: 'info',
  115. message: '已取消删除'
  116. });
  117. });
  118. },
  119. // 批量删除(取值)
  120. handleSelectionChange(val) {
  121. this.multipleSelection = val
  122. // console.log(val)
  123. },
  124. // 编辑职位
  125. doUpdate() {
  126. this.putRequest('/system/basic/pos/', this.updatePos).then(resp => {
  127. if (resp) {
  128. this.initPositions() // 刷新数据列表
  129. this.dialogVisible = false // 关闭对话框
  130. }
  131. })
  132. },
  133. // 编辑职位对话框
  134. showEditView(index, data) {
  135. Object.assign(this.updatePos, data) // 回显数据,数据拷贝
  136. // this.updatePos = data // 回显数据 有bug
  137. this.updatePos.createDate = ''
  138. this.dialogVisible = true // 显示编辑框
  139. },
  140. // 添加职位
  141. addPosition() {
  142. if (this.pos.name) {
  143. this.postRequest('/system/basic/pos/', this.pos).then(resp => {
  144. if (resp) {
  145. this.initPositions()
  146. this.pos.name = ''
  147. }
  148. })
  149. } else {
  150. this.$message.error('职位名称不能为空!')
  151. }
  152. },
  153. // 删除职位
  154. handleDelete(index, data) {
  155. this.$confirm('此操作将永久删除[' + data.name + ']职位, 是否继续?', '提示', {
  156. confirmButtonText: '确定',
  157. cancelButtonText: '取消',
  158. type: 'warning'
  159. }).then(() => {
  160. this.deleteRequest(' /system/basic/pos/' + data.id).then(resp => {
  161. if (resp) {
  162. this.initPositions()
  163. }
  164. })
  165. }).catch(() => {
  166. this.$message({
  167. type: 'info',
  168. message: '已取消删除'
  169. });
  170. });
  171. },
  172. // 获取后端接口所有职位数据
  173. initPositions() {
  174. this.getRequest('/system/basic/pos/').then(resp => {
  175. if (resp) {
  176. this.positions = resp
  177. }
  178. })
  179. }
  180. }
  181. }
  182. </script>
  183. <style scoped>
  184. /*添加职位输入框*/
  185. .addPosInput {
  186. width: 300px;
  187. margin-right: 8px;
  188. }
  189. /*所有数据表格*/
  190. .posManaMain {
  191. margin-top: 10px;
  192. }
  193. /*编号职位输入框*/
  194. .updatePosInput {
  195. width: 200px;
  196. margin-left: 8px;
  197. }
  198. </style>

7.6 职称管理JobLevelMana.vue组件

职称管理实现了职称的添加、更新、单条删除、批量删除功能。

单条更新里是否启用按钮使用了element的开关控件

 JobLevelMana.vue

  1. <template>
  2. <div>
  3. <div>
  4. <el-input size="small" v-model="jl.name" placeholder="添加职称名称..."
  5. prefix-icon="el-icon-plus" style="width: 300px"></el-input>
  6. <el-select size="small" v-model="jl.titleLevel" placeholder="职称等级" style="margin-left: 6px;margin-right: 6px">
  7. <el-option
  8. v-for="item in titleLevels"
  9. :key="item"
  10. :label="item"
  11. :value="item">
  12. </el-option>
  13. </el-select>
  14. <el-button type="primary" icon="el-icon-plus" size="small" @click="addJobLevel">添加</el-button>
  15. </div>
  16. <div style="margin-top: 10px;">
  17. <el-table
  18. :data="jls"
  19. stripe
  20. border
  21. size="small"
  22. style="width: 70%;"
  23. @selection-change="handleSelectionChange">
  24. <el-table-column
  25. type="selection"
  26. width="55">
  27. </el-table-column>
  28. <el-table-column
  29. prop="id"
  30. label="编号"
  31. width="55">
  32. </el-table-column>
  33. <el-table-column
  34. prop="name"
  35. label="职称名称"
  36. width="150">
  37. </el-table-column>
  38. <el-table-column
  39. prop="titleLevel"
  40. label="职称等级"
  41. width="150">
  42. </el-table-column>
  43. <el-table-column
  44. prop="createDate"
  45. label="创建日期"
  46. width="150">
  47. </el-table-column>
  48. <el-table-column
  49. prop="enabled"
  50. label="是否启用"
  51. width="100">
  52. <template slot-scope="scope">
  53. <el-tag type="success" v-if="scope.row.enabled">已启用</el-tag>
  54. <el-tag type="danger" v-else>未启用</el-tag>
  55. </template>
  56. </el-table-column>
  57. <el-table-column label="操作" width="350">
  58. <template slot-scope="scope">
  59. <el-button
  60. size="small"
  61. @click="showEditView(scope.row)">编辑
  62. </el-button>
  63. <el-button
  64. size="small"
  65. type="danger"
  66. @click="deleteHandle(scope.row)">删除
  67. </el-button>
  68. </template>
  69. </el-table-column>
  70. </el-table>
  71. <el-button size="small" style="margin-top: 8px" type="danger"
  72. :disabled="this.multipleSelection.length===0" @click="deleteMany">批量删除
  73. </el-button>
  74. </div>
  75. <!-- 编辑弹框 -->
  76. <el-dialog
  77. title="编辑职称"
  78. :visible.sync="dialogVisible"
  79. width="30%">
  80. <table>
  81. <tr>
  82. <td>
  83. <el-tag>职称名称</el-tag>
  84. </td>
  85. <td>
  86. <el-input v-model="updateJl.name" size="small" style="margin-left: 6px"></el-input>
  87. </td>
  88. </tr>
  89. <tr>
  90. <td>
  91. <el-tag>职称等级</el-tag>
  92. </td>
  93. <td>
  94. <el-select size="small" v-model="updateJl.titleLevel" placeholder="职称等级"
  95. style="margin-left: 6px;margin-right: 6px">
  96. <el-option
  97. v-for="item in titleLevels"
  98. :key="item"
  99. :label="item"
  100. :value="item">
  101. </el-option>
  102. </el-select>
  103. </td>
  104. </tr>
  105. <tr>
  106. <td>
  107. <el-tag>是否启用</el-tag>
  108. </td>
  109. <td>
  110. <el-switch
  111. style="margin-left: 6px"
  112. v-model="updateJl.enabled"
  113. active-color="#13ce66"
  114. inactive-color="#ff4949"
  115. active-text="启用"
  116. inactive-text="未启用">
  117. </el-switch>
  118. </td>
  119. </tr>
  120. </table>
  121. <span slot="footer" class="dialog-footer">
  122. <el-button @click="dialogVisible = false">取 消</el-button>
  123. <el-button type="primary" @click="doUpdate">确 定</el-button>
  124. </span>
  125. </el-dialog>
  126. </div>
  127. </template>
  128. <script>
  129. export default {
  130. name: "JobLevelMana",
  131. data() {
  132. return {
  133. // 查询 添加 数据对象
  134. jl: {
  135. name: '',
  136. titleLevel: ''
  137. },
  138. // 更新 数据对象
  139. updateJl: {
  140. name: '',
  141. titleLevel: '',
  142. enabled: false
  143. },
  144. titleLevels: [
  145. '正高级',
  146. '副高级',
  147. '中级',
  148. '初级',
  149. '员级'
  150. ],
  151. jls: [], // 删除单条
  152. dialogVisible: false,
  153. multipleSelection: [] // 批量删除勾选中的值
  154. }
  155. },
  156. mounted() {
  157. this.initJls()
  158. },
  159. methods: {
  160. // 执行批量删除
  161. deleteMany(){
  162. this.$confirm('此操作将永久删除[' + this.multipleSelection.length + ']条职称, 是否继续?', '提示', {
  163. confirmButtonText: '确定',
  164. cancelButtonText: '取消',
  165. type: 'warning'
  166. }).then(() => {
  167. let ids = '?'
  168. this.multipleSelection.forEach(item => {
  169. ids += 'ids=' + item.id + '&'
  170. })
  171. this.deleteRequest('/system/basic/joblevel/' + ids).then(resp => {
  172. if (resp) {
  173. this.initJls()
  174. }
  175. })
  176. }).catch(() => {
  177. this.$message({
  178. type: 'info',
  179. message: '已取消删除'
  180. });
  181. });
  182. },
  183. // 批量删除勾选中的值
  184. handleSelectionChange(val){
  185. this.multipleSelection = val
  186. },
  187. // 更新职称信息
  188. doUpdate() {
  189. this.putRequest('/system/basic/joblevel/', this.updateJl).then(resp => {
  190. if (resp) {
  191. this.initJls()
  192. this.dialogVisible = false
  193. }
  194. })
  195. },
  196. // 显示编辑弹框
  197. showEditView(data) {
  198. Object.assign(this.updateJl, data) // 复制数据,注意这里是 , 号隔开
  199. this.updateJl.createDate = '' // 更新日期由后端处理,这里不用传
  200. this.dialogVisible = true // 显示编辑弹框
  201. },
  202. // 删除职称
  203. deleteHandle(data) {
  204. this.$confirm('此操作将永久删除[' + data.name + ']职称, 是否继续?', '提示', {
  205. confirmButtonText: '确定',
  206. cancelButtonText: '取消',
  207. type: 'warning'
  208. }).then(() => {
  209. this.deleteRequest(' /system/basic/joblevel/' + data.id).then(resp => {
  210. if (resp) {
  211. this.initJls()
  212. }
  213. })
  214. }).catch(() => {
  215. this.$message({
  216. type: 'info',
  217. message: '已取消删除'
  218. });
  219. });
  220. },
  221. // 添加职称
  222. addJobLevel() {
  223. if (this.jl.name && this.jl.titleLevel) {
  224. this.postRequest('/system/basic/joblevel/', this.jl).then(resp => {
  225. if (resp) {
  226. this.initJls()
  227. }
  228. })
  229. } else {
  230. this.$message.error('字段不能为空!')
  231. }
  232. },
  233. // 获取职称列表数据
  234. initJls() {
  235. this.getRequest('/system/basic/joblevel/').then(resp => {
  236. if (resp) {
  237. this.jls = resp
  238. this.jl.name = ''
  239. this.jl.titleLevel = ''
  240. }
  241. })
  242. }
  243. }
  244. }
  245. </script>
  246. <style scoped>
  247. </style>

7.7 权限组PositionMana.vue组件

样式设计:由外到内依次使用了el-collapse折叠面板-》el-card卡片-》el-tree树形控件

<template slot="prepend">ROLE_</template>

使用element折叠面板展示角色,使用的是折叠面板的手风琴模式accordion,每次只能展开一个面板。

后端获取所有用户角色接口

与接口返回的nameZh角色字段绑定,展示在折叠面板上

折叠面板每个角色的菜单访问权限,采用树形控件设置角色权限。el-tree添加show-checkbox属性展示可选框。

默认选中的角色菜单列表根据角色rid获取后端接口返回的菜单selectedMenus。通过:default-checked-keys方式绑定。

使用getCheckedKeys获取选中节点key组成的数组。let selectedKeys = tree.getCheckedKeys(true) // 获取选中的节点数组,true 仅返回被选中的叶子节点的 keys,如三级分类

  1. <template>
  2. <div>
  3. <div class="positionManaTool">
  4. <el-input v-model="role.name" placeholder="请输入角色英文名" size="small">
  5. <template slot="prepend">ROLE_</template>
  6. </el-input>
  7. <el-input v-model="role.nameZh" placeholder="请输入角色中文名" size="small" @keydown.enter.native="doAddRole"></el-input>
  8. <el-button type="primary" icon="el-icon-plus" size="mini" @click="doAddRole">添加角色</el-button>
  9. </div>
  10. <!-- 手风琴 -->
  11. <div class="positionManaMain">
  12. <el-collapse v-model="activeName" accordion @change="change">
  13. <el-collapse-item :title="r.nameZh" :name="r.id" v-for="(r,index) in roles" :key="index">
  14. <el-card class="box-card">
  15. <div slot="header" class="clearfix">
  16. <span>可访问资源</span>
  17. <el-button type="text" icon="el-icon-delete" style="float: right;padding: 3px 0;color: #f41f0a" @click="doDeleteRole(r)">
  18. </el-button>
  19. </div>
  20. <div>
  21. <el-tree ref="treeRef" show-checkbox :data="allMenus" :props="defaultProps"
  22. :default-checked-keys="selectedMenus"
  23. node-key="id" :key="index"></el-tree>
  24. <div style="display: flex;justify-content: flex-end">
  25. <el-button size="mini" @click="cancelUpdate">取消修改</el-button>
  26. <el-button size="mini" type="primary" @click="doUpdate(r.id,index)">确认修改</el-button>
  27. </div>
  28. </div>
  29. </el-card>
  30. </el-collapse-item>
  31. </el-collapse>
  32. </div>
  33. </div>
  34. </template>
  35. <script>
  36. export default {
  37. name: "PositionMana",
  38. data() {
  39. return {
  40. role: {
  41. name: '',
  42. nameZh: ''
  43. },
  44. roles: [],
  45. allMenus: [],
  46. defaultProps: { // 树形控件
  47. children: 'children',
  48. label: 'name' // 绑定数据 :name="r.id"
  49. },
  50. selectedMenus: [],
  51. activeName: -1 // 折叠面板 默认关闭
  52. }
  53. },
  54. mounted() {
  55. this.initRoles()
  56. this.initAllMenus()
  57. },
  58. methods: {
  59. // 删除角色
  60. doDeleteRole(role){
  61. this.$confirm('此操作将永久删除[' + role.nameZh + ']角色, 是否继续?', '提示', {
  62. confirmButtonText: '确定',
  63. cancelButtonText: '取消',
  64. type: 'warning'
  65. }).then(() => {
  66. this.deleteRequest('/system/basic/permission/role/' + role.id).then(resp => {
  67. if (resp) {
  68. this.initRoles()
  69. }
  70. })
  71. }).catch(() => {
  72. this.$message({
  73. type: 'info',
  74. message: '已取消删除'
  75. });
  76. });
  77. },
  78. // 添加角色
  79. doAddRole(){
  80. if (this.role.name && this.role.nameZh) {
  81. this.postRequest('/system/basic/permission/role',this.role).then(resp=>{
  82. if (resp) {
  83. this.initRoles()
  84. this.role.name = ''
  85. this.role.nameZh = ''
  86. }
  87. })
  88. }else {
  89. this.$message.error('所有字段不能为空!')
  90. }
  91. },
  92. // 取消修改
  93. cancelUpdate() {
  94. this.activeName = -1 // 关闭折叠面板
  95. },
  96. // 确认修改
  97. doUpdate(rid, index) {
  98. let tree = this.$refs.treeRef[index] // 获取引用对象和索引
  99. let selectedKeys = tree.getCheckedKeys(true) // 获取选中的节点数组,true 仅返回被选中的叶子节点的 keys,如三级分类
  100. // console.log(selectedKeys)
  101. let url = '/system/basic/permission/?rid=' + rid
  102. selectedKeys.forEach(key => {
  103. // 循环遍历出数组 id ,拼接在一起
  104. url += '&mids=' + key
  105. })
  106. this.putRequest(url).then(resp => {
  107. if (resp) {
  108. this.activeName = -1 // 关闭折叠面板
  109. }
  110. })
  111. },
  112. // 手风琴点击事件
  113. change(rid) {
  114. if (rid) {
  115. this.initAllMenus() // 调用获取所有菜单
  116. this.initSelectedMenus(rid) // 调用获取所有选中的菜单
  117. // alert(rid) // :name="r.id" label: 'name'
  118. }
  119. },
  120. // 获取所有选中的菜单
  121. initSelectedMenus(rid) { // :name="r.id" change(rid)
  122. this.getRequest('/system/basic/permission/mid/' + rid).then(resp => {
  123. if (resp) {
  124. this.selectedMenus = resp
  125. }
  126. })
  127. },
  128. // 获取所有菜单
  129. initAllMenus() {
  130. this.getRequest('/system/basic/permission/menus').then(resp => {
  131. if (resp) {
  132. this.allMenus = resp
  133. }
  134. })
  135. },
  136. // 获取所有角色
  137. initRoles() {
  138. this.getRequest('/system/basic/permission/').then(resp => {
  139. if (resp) {
  140. this.roles = resp
  141. }
  142. })
  143. }
  144. }
  145. }
  146. </script>
  147. <style scoped>
  148. .positionManaTool {
  149. display: flex;
  150. justify-content: flex-start;
  151. }
  152. .positionManaTool .el-input {
  153. width: 300px;
  154. margin-right: 6px;
  155. }
  156. .positionManaMain {
  157. margin-top: 10px;
  158. width: 700px;
  159. }
  160. </style>

八、操作员管理

可以通过搜索操作员的名字,来单独显示操作员的信息。展示所有操作员的时候,不会把自己当前登录的操作员显示出来。

操作员涉及了权限:操作员拥有哪些角色,在根据角色再去拥有哪些菜单的权限。

获取操作员管理后端信息返回

 

SysAdmin.vue

  1. <template>
  2. <div>
  3. <!-- 1、 -->
  4. <div style="display: flex;justify-content: center;margin-top: 10px;">
  5. <!-- 9、v-model="keywords" \ @click="doSearch">搜索 -->
  6. <el-input v-model="keywords" placeholder="通过用户名搜索用户..." prefix-icon="el-icon-search"
  7. style="width: 400px;margin-right: 10px;"></el-input>
  8. <el-button type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
  9. </div>
  10. <!-- 2、6、 -->
  11. <div class="admin-container">
  12. <el-card class="admin-card" v-for="(admin,index) in admins" :key="index">
  13. <div slot="header" class="clearfix">
  14. <div class="userInfoTab">
  15. <div style="margin-top:5px;margin-right: 8px;">{{ admin.name}}</div>
  16. <div><img :src="admin.userFace" :alt="admin.name" :title="admin.name" class="userFace-img"></div>
  17. </div>
  18. <!-- 12、 @click="deleteAdmin(admin)" -->
  19. <el-button style="color:red;" type="text" icon="el-icon-delete"
  20. @click="deleteAdmin(admin)"></el-button>
  21. </div>
  22. <div class="userinfo-container">
  23. <div>用户名:{{ admin.name }}</div>
  24. <div>手机号码:{{ admin.phone }}</div>
  25. <div>电话号码:{{ admin.telephone }}</div>
  26. <div>地址:{{ admin.address }}</div>
  27. <div>用户状态:
  28. <!-- 14、更新操作员 @change="enabledChange(admin)" -->
  29. <el-switch
  30. v-model="admin.enabled"
  31. active-color="#13ce66"
  32. inactive-color="#ff4949"
  33. @change="enabledChange(admin)"
  34. active-text="启用"
  35. inactive-text="禁用">
  36. </el-switch>
  37. </div>
  38. <div>
  39. 用户角色:
  40. <el-tag style="margin-right: 4px;" type="success" v-for="(role,index) in admin.roles" :key="index">
  41. {{ role.nameZh }}
  42. </el-tag>
  43. <!-- 16、更新操作员角色 弹出框、选择器、 -->
  44. <!-- 20、@show="showPop(admin)" -->
  45. <!-- 24、@hide="hidePop(admin)" hide 隐藏时触发-->
  46. <el-popover
  47. placement="right"
  48. title="角色列表"
  49. width="200"
  50. @show="showPop(admin)"
  51. @hide="hidePop(admin)"
  52. trigger="click">
  53. <!-- 17、更新操作员角色 下拉框 -->
  54. <!-- 22、v-model="selectedRoles" 存的是1个角色id,multiple 多选,显示已有角色 -->
  55. <el-select v-model="selectedRoles" multiple placeholder="请选择">
  56. <el-option
  57. v-for="(r,index) in allRoles"
  58. :key="index"
  59. :label="r.nameZh"
  60. :value="r.id">
  61. </el-option>
  62. </el-select>
  63. <!-- 3个点按钮 ... -->
  64. <el-button slot="reference" type="text" icon="el-icon-more"></el-button>
  65. </el-popover>
  66. </div>
  67. <div>备注:{{ admin.remark }}</div>
  68. </div>
  69. </el-card>
  70. </div>
  71. </div>
  72. </template>
  73. <script>
  74. export default {
  75. name: "SysAdmin",
  76. data() {
  77. return {
  78. admins: [], // 3
  79. keywords: '', // 8、搜索关键字
  80. allRoles: [], // 18、更新操作员角色
  81. selectedRoles: [] // 23
  82. }
  83. },
  84. mounted() {
  85. this.initAdmins() // 5
  86. },
  87. methods: {
  88. // 25、更新操作员角色
  89. hidePop(admin) {
  90. let roles = []
  91. Object.assign(roles, admin.roles) // 拷贝对象
  92. let flag = false
  93. // 如果选中的角色 id 的长度和原来的不一样
  94. if (roles.length != this.selectedRoles.length) { // 用户对应角色id
  95. flag = true
  96. } else {
  97. // 角色 id 长度和原来的一样,但可能角色不一样
  98. // 先循环 admin.roles
  99. for (let i = 0; i < roles.length; i++) {
  100. let role = roles[i] // 用户对应的角色对象
  101. for (let j = 0; j < this.selectedRoles.length; j++) {
  102. let sr = this.selectedRoles[j] // 拿到用户对应的角色对象的id
  103. if (role.id == sr) { // 角色一样
  104. roles.splice(i, 1) // 删除
  105. i--
  106. break
  107. }
  108. }
  109. }
  110. if (roles.length != 0) {
  111. flag = true
  112. }
  113. }
  114. if (flag) {
  115. // 拼接 url(参数为 adminId、角色 rids )
  116. let url = '/system/admin/role?adminId=' + admin.id;
  117. this.selectedRoles.forEach(sr => {
  118. url += '&rids=' + sr
  119. });
  120. this.putRequest(url).then(resp => {
  121. if (resp) {
  122. this.initAdmins()
  123. }
  124. });
  125. }
  126. },
  127. // 21、下拉框获取所有用户角色
  128. showPop(admin) {
  129. this.initAllRoles()
  130. let roles = admin.roles // 拿到整个数组
  131. this.selectedRoles = []
  132. roles.forEach(r => {
  133. this.selectedRoles.push(r.id) // r.id 相同的角色放进数组
  134. })
  135. },
  136. // 19、获取所有操作员
  137. initAllRoles() {
  138. this.getRequest(' /system/admin/roles').then(resp => {
  139. if (resp) {
  140. this.allRoles = resp
  141. }
  142. })
  143. },
  144. // 15、更新操作员
  145. enabledChange(admin) {
  146. this.putRequest('/system/admin/', admin).then(resp => {
  147. if (resp) {
  148. this.initAdmins()
  149. }
  150. })
  151. },
  152. // 13、删除操作员
  153. deleteAdmin(admin) {
  154. this.$confirm('此操作将永久删除该[' + admin.name + '], 是否继续?', '提示', {
  155. confirmButtonText: '确定',
  156. cancelButtonText: '取消',
  157. type: 'warning'
  158. }).then(() => {
  159. this.deleteRequest('/system/admin/' + admin.id).then(resp => {
  160. if (resp) {
  161. this.initAdmins()
  162. }
  163. })
  164. }).catch(() => {
  165. this.$message({
  166. type: 'info',
  167. message: '已取消删除'
  168. });
  169. });
  170. },
  171. // 10 搜索
  172. doSearch() {
  173. this.initAdmins()
  174. },
  175. // 4、获取所有操作员;11、加参数关键字
  176. initAdmins() {
  177. this.getRequest('/system/admin/?keywords=' + this.keywords).then(resp => {
  178. if (resp) {
  179. this.admins = resp
  180. }
  181. })
  182. }
  183. }
  184. }
  185. </script>
  186. <style >
  187. /* 7 */
  188. .admin-container {
  189. margin-top: 10px;
  190. display: flex;
  191. justify-content: space-between;
  192. flex-wrap: wrap; /* 自动换行 */
  193. }
  194. .admin-card {
  195. width: 280px;
  196. margin-bottom: 20px;
  197. }
  198. .userInfoTab{
  199. /* background-color: black; */
  200. display: flex;
  201. justify-content: center;
  202. }
  203. /* 卡片顶部 */
  204. .clearfix {
  205. display: flex;
  206. justify-content: space-between;
  207. }
  208. .userFace-img {
  209. width: 36px;
  210. height: 36px;
  211. border-radius: 36px;
  212. }
  213. /* 头像居中 */
  214. /* .img-container {
  215. /* width: 100%;
  216. display: flex;
  217. justify-content: center;
  218. } */
  219. .userinfo-container {
  220. font-size: 12px;
  221. color: #3e9ef5;
  222. }
  223. </style>

九、员工资料

一、展示所有员工

二、分页展示

三、员工搜索

四、员工添加

五、更新和删除

六、导入导出数据

axios本身不提供下载功能,需要安装js-file-download

npm install js-file-download

以流的形式输出,流的格式是二进制数组。与axios的接口请求类似,js-file-download也需要封装请求拦截器和响应拦截器,因为js-file-download不共用axios封装的拦截器功能。请求拦截器需要重新设置对请求头Authorization的设置。axios的响应拦截器是对响应码进行判断,而js-file-download需要判断返回的是否是json字符串。通过判断返回头中的content-type,如果content-type是application/json格式,则是普通的json返回,需要将二进制编码转为普通的string形式。非json字符串才是流的形式返回,需要获取fileName,contentType。为了防止可能因为文件名是中文而造成乱码,需要将fileName进行格式转换。

  1. let fileDownload = require('js-file-download') // 插件
  2. let fileName = headers['content-disposition'].split(';')[1].split('filename=')[1]//文件名
  3. let contentType = headers['content-type'] // 响应类型
  4. fileName = decodeURIComponent(fileName) // 格式转换 防止乱码
  5. fileDownload(resp.data, fileName, contentType) // 通过插件下载文件

 EmpBasic.vue

  1. <template>
  2. <div>
  3. <div>
  4. <div style="display: flex;justify-content: space-between;">
  5. <!-- 1、 -->
  6. <!-- 20、搜索 v-model="empName" <el-input @keydown.enter.native="initEmps" 回车键调用初始化会员方法
  7. 21、@click="initEmps">搜索</el-button>
  8. 22、清空 clearable @clear="initEmps" -->
  9. <!-- 28-8 :disabled="showAdvanceSearchVisible" -->
  10. <div style="margin-top: 10px;">
  11. <el-input style="width: 300px;margin-right: 10px;"
  12. prefix-icon="el-icon-search"
  13. v-model="empName"
  14. placeholder="请输入员工名进行搜索..."
  15. @keydown.enter.native="initEmps"
  16. clearable
  17. @clear="initEmps"
  18. :disabled="showAdvanceSearchVisible"
  19. ></el-input>
  20. <el-button type="primary" icon="el-icon-search" @click="initEmps"
  21. :disabled="showAdvanceSearchVisible">搜索
  22. </el-button>
  23. <!-- 28-3 @click="showAdvanceSearchVisible = !showAdvanceSearchVisible" -->
  24. <!-- 28-5 判断图标样式 :class="showAdvanceSearchVisible?'fa fa-angle-double-up':'fa fa-angle-double-down'"-->
  25. <el-button type="primary" @click="showAdvanceSearchVisible = !showAdvanceSearchVisible">
  26. <i :class="showAdvanceSearchVisible?'fa fa-angle-double-up':'fa fa-angle-double-down'"
  27. aria-hidden="true"></i>高级搜索
  28. </el-button>
  29. </div>
  30. <div>
  31. <!-- 27-1、3 导入数据 上传组件 用自己的按钮 -->
  32. <!-- 27-5 on-success 文件上传成功时的钩子; on-error 文件上传失败时的钩子; -->
  33. <!-- 27-8 导入的时候禁用导入按钮 :disabled="importDataDisabled" -->
  34. <!-- 27-11 :headers="headers" 设置上传的请求头部 -->
  35. <el-upload style="display: inline-flex;margin-right: 8px;" :show-file-list="false"
  36. :headers="headers"
  37. :before-upload="beforeUpload"
  38. :on-success="onSuccess"
  39. :on-error="onError"
  40. :disabled="importDataDisabled"
  41. action="/employee/basic/import"
  42. >
  43. <el-button type="success" :icon="importDataBtnIcon" :disabled="importDataDisabled">{{
  44. importDataBtnText
  45. }}
  46. </el-button>
  47. </el-upload>
  48. <!-- 26-1、导出数据 @click="exportData" -->
  49. <el-button type="success" @click="exportData"><i class="el-icon-download" aria-hidden="true"></i> 导出数据
  50. </el-button>
  51. <!-- 23-3、 @click="showAddEmpView" -->
  52. <el-button type="primary" icon="el-icon-plus" @click="showAddEmpView">添加员工</el-button>
  53. </div>
  54. </div>
  55. <!-- 28-1 高级搜索条件框 -->
  56. <!-- 28-4 高级搜索条件框 v-show="showAdvanceSearchVisible" -->
  57. <!-- 28-6 添加展开动画效果 <transition name="fade"> 包含整个搜索条件框 </transition> -->
  58. <!-- 30-2 绑定搜索条件数据 v-model="searchValue.xxxxx" -->
  59. <transition name="slide-fade">
  60. <div v-show="showAdvanceSearchVisible"
  61. style="border: 1px solid #379ff5;border-radius: 5px;box-sizing: border-box;padding: 5px;margin: 10px 0;">
  62. <el-row>
  63. <el-col :span="5">
  64. 政治面貌:
  65. <el-select v-model="searchValue.politicId" placeholder="请选择政治面貌" size="mini" style="width: 130px;">
  66. <el-option
  67. v-for="item in politicsstatus"
  68. :key="item.id"
  69. :label="item.name"
  70. :value="item.id">
  71. </el-option>
  72. </el-select>
  73. </el-col>
  74. <el-col :span="4">
  75. 民族:
  76. <el-select v-model="searchValue.nationId" placeholder="民族" size="mini" style="width: 130px;">
  77. <el-option
  78. v-for="item in nations"
  79. :key="item.id"
  80. :label="item.name"
  81. :value="item.id">
  82. </el-option>
  83. </el-select>
  84. </el-col>
  85. <el-col :span="4">
  86. 职位:
  87. <el-select v-model="searchValue.posId" placeholder="职位" size="mini" style="width: 130px;">
  88. <el-option
  89. v-for="item in positions"
  90. :key="item.id"
  91. :label="item.name"
  92. :value="item.id">
  93. </el-option>
  94. </el-select>
  95. </el-col>
  96. <el-col :span="4">
  97. 职称:
  98. <el-select v-model="searchValue.jobLevelId" placeholder="职称" size="mini" style="width: 130px;">
  99. <el-option
  100. v-for="item in joblevels"
  101. :key="item.id"
  102. :label="item.name"
  103. :value="item.id">
  104. </el-option>
  105. </el-select>
  106. </el-col>
  107. <el-col :span="6">
  108. 聘用形式:
  109. <el-radio-group v-model="searchValue.engageForm">
  110. <el-radio label="劳动合同">劳动合同</el-radio>
  111. <el-radio label="劳务合同">劳务合同</el-radio>
  112. </el-radio-group>
  113. </el-col>
  114. </el-row>
  115. <el-row style="margin-top: 10px;">
  116. <!-- 30-4 处理部门 v-model="visible2" -->
  117. <el-col :span="5">
  118. 所属部门:
  119. <el-popover
  120. placement="bottom"
  121. title="请选择部门"
  122. width="220"
  123. trigger="manual"
  124. v-model="visible2">
  125. <!-- 23-20 添加树形控件 default-expand-all 是否默认展开所有节点 ,节点点击事件 @node-click="handleNodeClick" -->
  126. <el-tree :data="allDeps"
  127. :props="defaultProps"
  128. default-expand-all
  129. @node-click="searchHandleNodeClick"></el-tree>
  130. <!-- 30-6 @node-click="searchHandleNodeClick" -->
  131. <!-- node-click 节点被点击时的回调 共三个参数,依次为:传递给 data 属性的数组中该节点所对应的对象、节点对应的 Node、节点组件本身。 -->
  132. <!-- 自定义点击事件 -->
  133. <!-- 30-7 @click="showDepView2" -->
  134. <div slot="reference"
  135. style="width:130px;display: inline-flex;
  136. border-radius: 5px;border: 1px solid #dedede;height: 28px;cursor: pointer;align-items: center;
  137. font-size: 12px;padding-left: 8px;box-sizing: border-box;"
  138. @click="showDepView2">{{ inputDepName }}
  139. </div><!-- 23-25 回显数据 {{inputDepName}} -->
  140. </el-popover>
  141. </el-col>
  142. <!-- 30-3 处理日期:v-model="searchValue.beginDateScope" value-format="yyyy-MM-dd" ;
  143. 两个面板各自独立切换当前年份 使用unlink-panels -->
  144. <el-col :span="10">
  145. 入职日期:
  146. <el-date-picker
  147. unlink-panels
  148. size="mini"
  149. v-model="searchValue.beginDateScope"
  150. type="datetimerange"
  151. range-separator="至"
  152. value-format="yyyy-MM-dd"
  153. start-placeholder="开始日期"
  154. end-placeholder="结束日期">
  155. </el-date-picker>
  156. </el-col>
  157. <el-col :span="5" :offset="4">
  158. <el-button size="mini">取消</el-button>
  159. <!-- 30-10 @click="initEmps('advanced')" -->
  160. <el-button type="primary" icon="el-icon-search" size="mini" @click="initEmps('advanced')">搜索</el-button>
  161. </el-col>
  162. </el-row>
  163. </div>
  164. </transition>
  165. </div>
  166. <div style="margin-top: 10px;">
  167. <!-- 2、表格;6、添加 loading -->
  168. <el-table
  169. :data="emps"
  170. v-loading="loading"
  171. element-loading-text="拼命加载中"
  172. element-loading-spinner="el-icon-loading"
  173. element-loading-background="rgba(0, 0, 0, 0.8)"
  174. style="width: 100%" stripe border>
  175. <el-table-column
  176. type="selection"
  177. width="55">
  178. </el-table-column>
  179. <el-table-column
  180. prop="name"
  181. label="姓名"
  182. align="left"
  183. fixed
  184. width="90">
  185. </el-table-column>
  186. <el-table-column
  187. prop="gender"
  188. label="性别"
  189. align="left"
  190. width="40">
  191. </el-table-column>
  192. <el-table-column
  193. prop="workId"
  194. label="工号"
  195. align="left"
  196. width="85">
  197. </el-table-column>
  198. <el-table-column
  199. prop="birthday"
  200. label="出生日期"
  201. align="left"
  202. width="95">
  203. </el-table-column>
  204. <el-table-column
  205. prop="idCard"
  206. label="身份证号"
  207. width="150">
  208. </el-table-column>
  209. <el-table-column
  210. prop="wedlock"
  211. label="婚姻状态"
  212. align="center"
  213. width="70">
  214. </el-table-column>
  215. <el-table-column
  216. prop="nation.name"
  217. label="民族"
  218. align="left"
  219. width="50">
  220. </el-table-column>
  221. <el-table-column
  222. prop="nativePlace"
  223. label="籍贯"
  224. align="center"
  225. width="80">
  226. </el-table-column>
  227. <el-table-column
  228. prop="politicsStatus.name"
  229. label="政治面貌"
  230. width="100">
  231. </el-table-column>
  232. <el-table-column
  233. prop="email"
  234. label="电子邮件"
  235. align="left"
  236. width="150">
  237. </el-table-column>
  238. <el-table-column
  239. prop="phone"
  240. label="电话号码"
  241. align="left"
  242. width="100">
  243. </el-table-column>
  244. <el-table-column
  245. prop="address"
  246. label="联系地址"
  247. align="center"
  248. width="220">
  249. </el-table-column>
  250. <el-table-column
  251. prop="department.name"
  252. label="所属部门"
  253. align="left"
  254. width="100">
  255. </el-table-column>
  256. <el-table-column
  257. prop="position.name"
  258. label="职位"
  259. width="100">
  260. </el-table-column>
  261. <el-table-column
  262. prop="joblevel.name"
  263. label="级别"
  264. width="100">
  265. </el-table-column>
  266. <el-table-column
  267. prop="engageForm"
  268. label="聘用形式"
  269. align="left"
  270. width="100">
  271. </el-table-column>
  272. <el-table-column
  273. prop="tiptopDegree"
  274. label="最高学历"
  275. align="center"
  276. width="80">
  277. </el-table-column>
  278. <el-table-column
  279. prop="school"
  280. label="毕业学校"
  281. align="left"
  282. width="150">
  283. </el-table-column>
  284. <el-table-column
  285. prop="specialty"
  286. label="所属专业"
  287. align="left"
  288. width="150">
  289. </el-table-column>
  290. <el-table-column
  291. prop="workState"
  292. label="在职状态"
  293. align="center"
  294. width="70">
  295. </el-table-column>
  296. <el-table-column
  297. prop="beginDate"
  298. label="入职日期"
  299. align="left"
  300. width="95">
  301. </el-table-column>
  302. <el-table-column
  303. prop="conversionTime"
  304. label="转正日期"
  305. align="left"
  306. width="95">
  307. </el-table-column>
  308. <el-table-column
  309. prop="beginContract"
  310. label="合同起始日期"
  311. align="left"
  312. width="95">
  313. </el-table-column>
  314. <el-table-column
  315. prop="endContract"
  316. label="合同截止日期"
  317. align="left"
  318. width="95">
  319. </el-table-column>
  320. <el-table-column
  321. label="合同期限"
  322. align="left"
  323. width="100">
  324. <template slot-scope="scope">
  325. <el-tag>{{ scope.row.contractTerm }}</el-tag>
  326. </template>
  327. </el-table-column>
  328. <el-table-column
  329. label="操作"
  330. fixed="right"
  331. width="200">
  332. <template slot-scope="scope">
  333. <!-- 25-4 给编辑按钮绑定点击事件 @click="showEmpView(scope.row)" -->
  334. <el-button style="padding: 3px;" size="mini" @click="showEmpView(scope.row)">编辑</el-button>
  335. <!-- <el-button style="padding: 3px;" size="mini" type="primary" plain>查看高级资料</el-button> -->
  336. <!-- 24-1 删除员工 @click="deleteEmp(scope.row)" -->
  337. <el-button style="padding: 3px;" size="mini" type="danger" @click="deleteEmp(scope.row)">删除</el-button>
  338. </template>
  339. </el-table-column>
  340. </el-table>
  341. <!-- 10、分页 -->
  342. <div style="display: flex;justify-content: flex-end;margin-top: 10px;">
  343. <!-- 13、@current-change="currentChange" 当前页
  344. 14、@size-change="sizeChange" 每页显示多少条 -->
  345. <el-pagination
  346. prev-text="上一页"
  347. next-text="下一页"
  348. @current-change="currentChange"
  349. @size-change="sizeChange"
  350. :page-sizes="[10,20,30,50,100]"
  351. layout="total, sizes, prev, pager, next, jumper"
  352. :total="total" background>
  353. </el-pagination>
  354. </div>
  355. </div>
  356. <!-- 23-1、开始- 添加员工弹框 -->
  357. <!-- 25-1 编辑员工 将添加员工弹框标题改为变量 根据条件显示是添加还是编辑 :title="title" -->
  358. <el-dialog
  359. :title="title"
  360. :visible.sync="dialogVisible"
  361. width="80%">
  362. <div>
  363. <!-- 23-6、<el-row <el-form -->
  364. <!-- 23-28 数据校验对象 :rules="empRules" ,每项属性对应 prop="posId" -->
  365. <el-form ref="empRef" :model="emp" :rules="empRules">
  366. <el-row>
  367. <el-col :span="6">
  368. <el-form-item label="姓名:" prop="name">
  369. <el-input v-model="emp.name" prefix-icon="el-icon-edit" placeholder="请输入员工姓名" size="mini"
  370. style="width: 150px;"></el-input>
  371. </el-form-item>
  372. </el-col>
  373. <el-col :span="5">
  374. <el-form-item label="性别:" prop="gender">
  375. <el-radio-group v-model="emp.gender" style="margin-top: 8px;">
  376. <el-radio label="男"></el-radio>
  377. <el-radio label="女"></el-radio>
  378. </el-radio-group>
  379. </el-form-item>
  380. </el-col>
  381. <el-col :span="6">
  382. <el-form-item label="出生日期:" prop="birthday">
  383. <el-date-picker
  384. v-model="emp.birthday"
  385. type="date"
  386. value-format="yyyy-MM-dd"
  387. size="mini"
  388. style="width: 150px;"
  389. placeholder="出生日期">
  390. </el-date-picker>
  391. </el-form-item>
  392. </el-col>
  393. <el-col :span="7">
  394. <!-- 23-10、 添加员工 给每项赋值 -->
  395. <el-form-item label="政治面貌:" prop="politicId">
  396. <el-select v-model="emp.politicId" placeholder="请选择政治面貌" size="mini" style="width: 200px;">
  397. <el-option
  398. v-for="item in politicsstatus"
  399. :key="item.id"
  400. :label="item.name"
  401. :value="item.id">
  402. </el-option>
  403. </el-select>
  404. </el-form-item>
  405. </el-col>
  406. </el-row>
  407. <el-row>
  408. <el-col :span="6">
  409. <el-form-item label="民族:" prop="nationId">
  410. <el-select v-model="emp.nationId" placeholder="民族" size="mini" style="width: 150px;">
  411. <el-option
  412. v-for="item in nations"
  413. :key="item.id"
  414. :label="item.name"
  415. :value="item.id">
  416. </el-option>
  417. </el-select>
  418. </el-form-item>
  419. </el-col>
  420. <el-col :span="5">
  421. <el-form-item label="籍贯:" prop="nativePlace">
  422. <el-input v-model="emp.nativePlace" placeholder="籍贯" prefix-icon="el-icon-edit" size="small"
  423. style="width: 120px;"></el-input>
  424. </el-form-item>
  425. </el-col>
  426. <el-col :span="6">
  427. <el-form-item label="电子邮箱:" prop="email">
  428. <el-input v-model="emp.email" placeholder="请输入电子邮箱" prefix-icon="el-icon-message" size="mini"
  429. style="width: 150px;"></el-input>
  430. </el-form-item>
  431. </el-col>
  432. <el-col :span="7">
  433. <el-form-item label="联系地址:" prop="address">
  434. <el-input v-model="emp.address" placeholder="请输入联系地址" prefix-icon="el-icon-edit" size="mini"
  435. style="width: 200px;"></el-input>
  436. </el-form-item>
  437. </el-col>
  438. </el-row>
  439. <el-row>
  440. <el-col :span="6">
  441. <el-form-item label="职位:" prop="posId">
  442. <el-select v-model="emp.posId" placeholder="职位" size="mini" style="width: 150px;">
  443. <el-option
  444. v-for="item in positions"
  445. :key="item.id"
  446. :label="item.name"
  447. :value="item.id">
  448. </el-option>
  449. </el-select>
  450. </el-form-item>
  451. </el-col>
  452. <el-col :span="5">
  453. <el-form-item label="职称:" prop="jobLevelId">
  454. <el-select v-model="emp.jobLevelId" placeholder="职称" size="mini" style="width: 150px;">
  455. <el-option
  456. v-for="item in joblevels"
  457. :key="item.id"
  458. :label="item.name"
  459. :value="item.id">
  460. </el-option>
  461. </el-select>
  462. </el-form-item>
  463. </el-col>
  464. <el-col :span="6">
  465. <!-- 23-15 -->
  466. <el-form-item label="所属部门:" prop="departmentId">
  467. <!-- 23-17 manual 手动弹出框 -->
  468. <el-popover
  469. placement="bottom"
  470. title="请选择部门"
  471. width="200"
  472. trigger="manual"
  473. v-model="visible">
  474. <!-- 23-20 添加树形控件 default-expand-all 是否默认展开所有节点 ,节点点击事件 @node-click="handleNodeClick" -->
  475. <el-tree :data="allDeps"
  476. :props="defaultProps"
  477. default-expand-all
  478. @node-click="handleNodeClick"></el-tree>
  479. <!-- node-click 节点被点击时的回调 共三个参数,依次为:传递给 data 属性的数组中该节点所对应的对象、节点对应的 Node、节点组件本身。 -->
  480. <!-- 自定义点击事件 -->
  481. <div slot="reference"
  482. style="width:150px;display: inline-flex;
  483. border-radius: 5px;border: 1px solid #dedede;height: 28px;cursor: pointer;align-items: center;
  484. font-size: 12px;padding-left: 8px;box-sizing: border-box;"
  485. @click="showDepView">{{ inputDepName }}
  486. </div><!-- 23-25 回显数据 {{inputDepName}} -->
  487. </el-popover>
  488. </el-form-item>
  489. </el-col>
  490. <el-col :span="7">
  491. <el-form-item label="电话号码:" prop="phone">
  492. <el-input v-model="emp.phone" placeholder="请输入电话号码" size="mini" style="width: 200px;"
  493. prefix-icon="el-icon-phone"></el-input>
  494. </el-form-item>
  495. </el-col>
  496. </el-row>
  497. <el-row>
  498. <el-col :span="6">
  499. <el-form-item label="工号:" prop="workId">
  500. <el-input v-model="emp.workId" placeholder="请输入工号" size="mini" style="width: 150px;"
  501. prefix-icon="el-icon-edit" disabled></el-input>
  502. </el-form-item>
  503. </el-col>
  504. <el-col :span="5">
  505. <!-- 23-14 数据在 data 中写死的 -->
  506. <el-form-item label="学历:" prop="tiptopDegree">
  507. <el-select v-model="emp.tiptopDegree" placeholder="职称" size="mini" style="width: 150px;">
  508. <el-option
  509. v-for="item in tiptopDegrees"
  510. :key="item"
  511. :label="item"
  512. :value="item">
  513. </el-option>
  514. </el-select>
  515. </el-form-item>
  516. </el-col>
  517. <el-col :span="6">
  518. <el-form-item label="毕业院校:" prop="school">
  519. <el-input v-model="emp.school" placeholder="请输入学校" size="mini" style="width: 150px;"
  520. prefix-icon="el-icon-edit"></el-input>
  521. </el-form-item>
  522. </el-col>
  523. <el-col :span="7">
  524. <el-form-item label="专业名称:" prop="specialty">
  525. <el-input v-model="emp.specialty" placeholder="请输入专业名称" size="mini" style="width: 200px;"
  526. prefix-icon="el-icon-edit"></el-input>
  527. </el-form-item>
  528. </el-col>
  529. </el-row>
  530. <el-row>
  531. <el-col :span="6">
  532. <el-form-item label="入职日期:" prop="beginDate">
  533. <el-date-picker
  534. v-model="emp.beginDate"
  535. type="date"
  536. value-format="yyyy-MM-dd"
  537. size="mini"
  538. style="width: 120px;"
  539. placeholder="入职日期">
  540. </el-date-picker>
  541. </el-form-item>
  542. </el-col>
  543. <el-col :span="5">
  544. <el-form-item label="转正日期:" prop="conversionTime">
  545. <el-date-picker
  546. v-model="emp.conversionTime"
  547. type="date"
  548. value-format="yyyy-MM-dd"
  549. size="mini"
  550. style="width: 120px;"
  551. placeholder="转正日期">
  552. </el-date-picker>
  553. </el-form-item>
  554. </el-col>
  555. <el-col :span="6">
  556. <el-form-item label="合同起始日期:" prop="beginContract">
  557. <el-date-picker
  558. v-model="emp.beginContract"
  559. type="date"
  560. value-format="yyyy-MM-dd"
  561. size="mini"
  562. style="width: 135px;"
  563. placeholder="合同起始日期">
  564. </el-date-picker>
  565. </el-form-item>
  566. </el-col>
  567. <el-col :span="7">
  568. <el-form-item label="合同截止日期:" prop="endContract">
  569. <el-date-picker
  570. v-model="emp.endContract"
  571. type="date"
  572. value-format="yyyy-MM-dd"
  573. size="mini"
  574. style="width: 170px;"
  575. placeholder="合同截止日期">
  576. </el-date-picker>
  577. </el-form-item>
  578. </el-col>
  579. </el-row>
  580. <el-row>
  581. <el-col :span="8">
  582. <el-form-item label="身份证号码:" prop="idCard">
  583. <el-input v-model="emp.idCard" placeholder="请输入身份证号码"
  584. size="mini" prefix-icon="el-icon-edit" style="width: 180px;"></el-input>
  585. </el-form-item>
  586. </el-col>
  587. <el-col :span="8">
  588. <el-form-item label="聘用形式:" prop="engageForm">
  589. <el-radio-group v-model="emp.engageForm" style="margin-top: 8px;">
  590. <el-radio label="劳动合同">劳动合同</el-radio>
  591. <el-radio label="劳务合同">劳务合同</el-radio>
  592. </el-radio-group>
  593. </el-form-item>
  594. </el-col>
  595. <el-col :span="8">
  596. <el-form-item label="婚姻状况:" prop="wedlock">
  597. <el-radio-group v-model="emp.wedlock" style="margin-top: 8px;">
  598. <el-radio label="未婚">未婚</el-radio>
  599. <el-radio label="已婚">已婚</el-radio>
  600. <el-radio label="离异">离异</el-radio>
  601. </el-radio-group>
  602. </el-form-item>
  603. </el-col>
  604. </el-row>
  605. </el-form>
  606. </div>
  607. <span slot="footer" class="dialog-footer">
  608. <el-button @click="dialogVisible = false">取 消</el-button>
  609. <!-- 23-26 @click="doAddEmp"-->
  610. <el-button type="primary" @click="doAddEmp">确 定</el-button>
  611. </span>
  612. </el-dialog>
  613. </div>
  614. </template>
  615. <script>
  616. export default {
  617. name: "EmpBasic",
  618. data() {
  619. return {
  620. searchValue: { // 30-1 高级搜索 条件对象
  621. politicId: null, // 政治面貌
  622. nationId: null, // 民族
  623. posId: null, // 职位
  624. jobLevelId: null, // 职称
  625. engageForm: '', // 聘用形式
  626. departmentId: null, // 部门 id
  627. beginDateScope: null // 入职日期范围
  628. },
  629. showAdvanceSearchVisible: false, // 28-2 高级搜索框 动态效果
  630. headers: { // 27-12 定义请求头
  631. Authorization: window.sessionStorage.getItem('tokenStr')
  632. },
  633. importDataDisabled: false, // 27-9 导入按钮 默认不禁用
  634. importDataBtnText: '导入数据', // 27-2 导入数据
  635. importDataBtnIcon: 'el-icon-upload2', // 27-2 导入数据
  636. title: '', // 25-2 添加编辑员工弹框动态标题
  637. emps: [], // 3、获取所有员工(分页)
  638. loading: false, // 7、添加 loading
  639. total: 0, // 11 分页总条数
  640. currentPage: 1, // 14、默认显示第1页(currentPage 后端字段)
  641. size: 10, // 15、默认每页显示 10 条
  642. empName: '', // 18、搜索
  643. dialogVisible: false, // 23-2、添加员工弹框
  644. nations: [], // 23-7 添加员工 民族
  645. joblevels: [], // 23-7 职称
  646. politicsstatus: [], // 23-7 政治面貌
  647. positions: [], // 23-7 职位
  648. department: [], // 部门
  649. // 23-13、学历
  650. tiptopDegrees: ['博士', '硕士', '本科', '大专', '高中', '初中', '小学', '其它'],
  651. // 23-5、添加员工
  652. emp: {
  653. id: null,
  654. name: '',
  655. gender: '',
  656. birthday: '',
  657. idCard: '',
  658. wedlock: '',
  659. nationId: null,
  660. nativePlace: '',
  661. politicId: null,
  662. email: '',
  663. phone: '',
  664. address: '',
  665. departmentId: null,
  666. jobLevelId: null,
  667. posId: null,
  668. engageForm: '',
  669. tiptopDegree: '',
  670. specialty: '',
  671. school: '',
  672. beginDate: '',
  673. workState: '在职',
  674. workId: '',
  675. contractTerm: null,
  676. conversionTime: '',
  677. notworkDate: null,
  678. beginContract: '',
  679. endContract: '',
  680. workAge: null,
  681. salaryId: null
  682. },
  683. visible: false, // 23-18 弹出框
  684. visible2: false, // 30-5 高级搜索 部门
  685. // 23-21 树形控件
  686. defaultProps: {
  687. children: 'children',
  688. label: 'name'
  689. },
  690. allDeps: [], // 23-21 树形控件 绑定 所属部门 数据对象
  691. inputDepName: '',// 23-23 回显部门数据
  692. // 23-30 表单数据校验
  693. empRules: {
  694. name: [{required: true, message: '请输入员工名', trigger: 'blur'}],
  695. gender: [{required: true, message: '请输入员工性别', trigger: 'blur'}],
  696. birthday: [{required: true, message: '请输入出生日期', trigger: 'blur'}],
  697. idCard: [{required: true, message: '请输入身份证号码', trigger: 'blur'},
  698. {
  699. pattern: /(^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$)|(^[1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{2}$)/,
  700. message: '身份证号码不正确', trigger: 'blur'
  701. }],
  702. wedlock: [{required: true, message: '请输入婚姻状况', trigger: 'blur'}],
  703. nationId: [{required: true, message: '请输入民族', trigger: 'blur'}],
  704. nativePlace: [{required: true, message: '请输入籍贯', trigger: 'blur'}],
  705. politicId: [{required: true, message: '请输入政治面貌', trigger: 'blur'}],
  706. email: [{required: true, message: '请输入邮箱地址', trigger: 'blur'},
  707. {type: 'email', message: '邮箱地址格式不正确', trigger: 'blur'}],
  708. phone: [{required: true, message: '请输入电话号码', trigger: 'blur'},
  709. {
  710. pattern: /^(0|86|17951)?(13[0-9]|15[012356789]|17[678]|18[0-9]|14[57])[0-9]{8}$/,
  711. message: '请输入合法手机号码', trigger: 'blur'
  712. }],
  713. address: [{required: true, message: '请输入地址', trigger: 'blur'}],
  714. departmentId: [{required: true, message: '请输入部门名称', trigger: 'blur'}],
  715. jobLevelId: [{required: true, message: '请输入职称', trigger: 'blur'}],
  716. posId: [{required: true, message: '请输入职位', trigger: 'blur'}],
  717. engageForm: [{required: true, message: '请输入聘用形式', trigger: 'blur'}],
  718. tiptopDegree: [{required: true, message: '请输入学历', trigger: 'blur'}],
  719. specialty: [{required: true, message: '请输入专业', trigger: 'blur'}],
  720. school: [{required: true, message: '请输入毕业院校', trigger: 'blur'}],
  721. beginDate: [{required: true, message: '请输入入职日期', trigger: 'blur'}],
  722. workState: [{required: true, message: '请输入工作状态', trigger: 'blur'}],
  723. workId: [{required: true, message: '请输入工号', trigger: 'blur'}],
  724. contractTerm: [{required: true, message: '请输入合同期限', trigger: 'blur'}],
  725. conversionTime: [{required: true, message: '请输入转正日期', trigger: 'blur'}],
  726. notworkDate: [{required: true, message: '请输入离职日期', trigger: 'blur'}],
  727. beginContract: [{required: true, message: '请输入合同起始日期', trigger: 'blur'}],
  728. endContract: [{required: true, message: '请输入合同结束日期', trigger: 'blur'}],
  729. workAge: [{required: true, message: '请输入工龄', trigger: 'blur'}]
  730. }
  731. }
  732. },
  733. mounted() {
  734. this.initEmps() // 5、获取所有员工(分页)
  735. this.initData() // 23-9 添加员工
  736. this.initPositions() // 23-12 获取职位
  737. },
  738. methods: {
  739. // 27-6 数据导入成功 恢复原来的图标和状态
  740. onSuccess() {
  741. this.importDataBtnIcon = 'el-icon-upload2'
  742. this.importDataBtnText = '导入数据'
  743. this.importDataDisabled = false // 29-10 不禁用导入按钮
  744. this.initEmps()
  745. },
  746. // 27-7 数据导入失败 恢复原来的图标和状态
  747. onError() {
  748. this.importDataBtnIcon = 'el-icon-upload2'
  749. this.importDataBtnText = '导入数据'
  750. this.importDataDisabled = false // 29-10 不禁用导入按钮
  751. },
  752. // 27-4、导入数据 改变图标和添加 loading 状态
  753. beforeUpload() {
  754. this.importDataBtnIcon = 'el-icon-loading'
  755. this.importDataBtnText = '正在导入'
  756. this.importDataDisabled = true // 29-10 禁用导入按钮
  757. },
  758. // 26-2 下载请求
  759. exportData() {
  760. this.downloadRequest('/employee/basic/export')
  761. },
  762. // 25-5 编辑员工按钮 点击事件
  763. showEmpView(data) {
  764. this.title = '编辑员工信息'
  765. this.emp = data // 回显数据
  766. this.inputDepName = data.department.name // 25-7 回显部门信息
  767. this.initPositions() // 25-9 初始化职位信息
  768. this.dialogVisible = true
  769. },
  770. // 24-2 删除员工
  771. deleteEmp(data) {
  772. this.$confirm('此操作将永久删除该员工' + data.name + ', 是否继续?', '提示', {
  773. confirmButtonText: '确定',
  774. cancelButtonText: '取消',
  775. type: 'warning'
  776. }).then(() => {
  777. this.deleteRequest('/employee/basic/' + data.id).then(resp => {
  778. if (resp) {
  779. this.initEmps()
  780. }
  781. })
  782. }).catch(() => {
  783. this.$message({
  784. type: 'info',
  785. message: '已取消删除'
  786. });
  787. });
  788. },
  789. // 23-27 确定添加员工
  790. // 25-10 添加或编辑员工 有id编辑员工 没有id添加员工
  791. // 添加和编辑这里就请求方式不一样 putRequest postRequest ,其它的都一样
  792. doAddEmp() {
  793. if (this.emp.id) {
  794. // 有 id 编辑员工
  795. this.$refs['empRef'].validate(valid => {
  796. if (valid) {
  797. this.putRequest('/employee/basic/', this.emp).then(resp => {
  798. if (resp) {
  799. this.dialogVisible = false
  800. this.initEmps()
  801. }
  802. })
  803. }
  804. })
  805. } else {
  806. // 没有id 添加员工
  807. // empRef 表单中定义的引用对象 ref="empRef"
  808. this.$refs['empRef'].validate(valid => {
  809. if (valid) {
  810. this.postRequest('/employee/basic/', this.emp).then(resp => {
  811. if (resp) {
  812. this.dialogVisible = false
  813. this.initEmps()
  814. }
  815. })
  816. }
  817. })
  818. }
  819. },
  820. // 30-7 高级搜索 部门点击事件
  821. searchHandleNodeClick(data) {
  822. this.inputDepName = data.name
  823. this.searchValue.departmentId = data.id
  824. this.visible2 = !this.visible2 // 弹框
  825. },
  826. // 23-22、24 树控件节点点击事件
  827. handleNodeClick(data) {
  828. this.inputDepName = data.name
  829. this.emp.departmentId = data.id
  830. this.visible = !this.visible // 弹框
  831. },
  832. // 30-9 高级搜索 部门弹框
  833. showDepView2() {
  834. this.visible2 = !this.visible2
  835. },
  836. // 23-16 添加员工 所属部门
  837. showDepView() {
  838. this.visible = !this.visible // 23-19 弹出框
  839. },
  840. // 23-13 添加员工 获取最大号
  841. getMaxworkId() {
  842. this.getRequest('/employee/basic/maxWorkID').then(resp => {
  843. if (resp) {
  844. this.emp.workId = resp.obj
  845. }
  846. })
  847. },
  848. // 23-11、 添加员工 获取职位 有可能变动 打开对话框的时候调用此方法
  849. initPositions() {
  850. this.getRequest('/employee/basic/Positions').then(resp => {
  851. if (resp) {
  852. this.positions = resp
  853. }
  854. })
  855. },
  856. // 23-8、添加员工 不怎么变动的数据。放 sessionStorage ,就不用怎么去查
  857. initData() {
  858. // 获取民族数据:先从 sessionStorage 里取,取不到再调用接口获取数据
  859. if (!window.sessionStorage.getItem("nations")) {
  860. this.getRequest('/employee/basic/nations').then(resp => {
  861. this.nations = resp
  862. // 存到 sessionStorage 里,把对象转字符串
  863. window.sessionStorage.setItem('nations', JSON.stringify(resp))
  864. })
  865. } else {
  866. // 从 sessionStorage 获取,字符串转对象
  867. this.nations = JSON.parse(window.sessionStorage.getItem('nations'))
  868. }
  869. // 获取职称
  870. if (!window.sessionStorage.getItem('joblevels')) {
  871. this.getRequest('/employee/basic/joblevels').then(resp => {
  872. if (resp) {
  873. this.joblevels = resp
  874. window.sessionStorage.setItem('joblevels', JSON.stringify(resp))
  875. }
  876. })
  877. } else {
  878. // 从 sessionStorage 获取,字符串转对象
  879. this.joblevels = JSON.parse(window.sessionStorage.getItem('joblevels'))
  880. }
  881. // 获取政治面貌
  882. if (!window.sessionStorage.getItem('politicsstatus')) {
  883. this.getRequest('/employee/basic/politicsStatus').then(resp => {
  884. if (resp) {
  885. this.politicsstatus = resp
  886. window.sessionStorage.setItem('politicsstatus', JSON.stringify(resp))
  887. }
  888. })
  889. } else {
  890. // 从 sessionStorage 获取,字符串转对象
  891. this.politicsstatus = JSON.parse(window.sessionStorage.getItem('politicsstatus'))
  892. }
  893. // 23-22 树形控件 绑定 所属部门 数据对象
  894. if (!window.sessionStorage.getItem('allDeps')) {
  895. this.getRequest('/employee/basic/deps').then(resp => {
  896. if (resp) {
  897. this.allDeps = resp
  898. window.sessionStorage.setItem('allDeps', JSON.parse(resp))
  899. }
  900. })
  901. } else {
  902. this.allDeps = window.sessionStorage.getItem('allDeps')
  903. }
  904. },
  905. // 23-4、添加员点击事件
  906. showAddEmpView() {
  907. // 25-6 清空表单
  908. this.emp = {
  909. id: null,
  910. name: '',
  911. gender: '',
  912. birthday: '',
  913. idCard: '',
  914. wedlock: '',
  915. nationId: null,
  916. nativePlace: '',
  917. politicId: null,
  918. email: '',
  919. phone: '',
  920. address: '',
  921. departmentId: null,
  922. jobLevelId: null,
  923. posId: null,
  924. engageForm: '',
  925. tiptopDegree: '',
  926. specialty: '',
  927. school: '',
  928. beginDate: '',
  929. workState: '在职',
  930. workId: '',
  931. contractTerm: null,
  932. conversionTime: '',
  933. notworkDate: null,
  934. beginContract: '',
  935. endContract: '',
  936. workAge: null,
  937. salaryId: null
  938. }
  939. this.inputDepName = '' // 25-8 清空部门信息
  940. this.title = '添加员工' // 25-3 点击添加员工按钮时,弹出框标题为 添加员工
  941. this.getMaxworkId() // 23-14 获取最大工号
  942. this.initPositions() // 23-12 获取职位
  943. this.dialogVisible = true
  944. },
  945. // 15、分页 每页显示多少条 默认会把 size 传进来
  946. sizeChange(size) {
  947. this.size = size
  948. this.initEmps()
  949. },
  950. // 13、分页-当前页-currentPage 点击的时候自己会带过来
  951. currentChange(currentPage) {
  952. this.currentPage = currentPage // 16
  953. this.initEmps() // 18、调用方法
  954. },
  955. // 4、获取所有员工(分页)
  956. initEmps(type) {
  957. this.loading = true // 8、添加 loading
  958. // 30-11 定义高级搜索 url
  959. let url = '/employee/basic/?currentPage=' + this.currentPage + '&size=' + this.size
  960. if (type && type === 'advanced') { // 说明是高级搜索
  961. if (this.searchValue.politicId) {
  962. url += '&politicId=' + this.searchValue.politicId
  963. }
  964. if (this.searchValue.nationId) {
  965. url += '&nationId=' + this.searchValue.nationId
  966. }
  967. if (this.searchValue.posId) {
  968. url += '&posId=' + this.searchValue.posId
  969. }
  970. if (this.searchValue.jobLevelId) {
  971. url += '&jobLevelId=' + this.searchValue.jobLevelId
  972. }
  973. if (this.searchValue.engageForm) {
  974. url += '&engageForm=' + this.searchValue.engageForm
  975. }
  976. if (this.searchValue.departmentId) {
  977. url += '&departmentId=' + this.searchValue.departmentId
  978. }
  979. if (this.searchValue.beginDateScope) {
  980. url += '&beginDateScope=' + this.searchValue.beginDateScope
  981. }
  982. } else {
  983. url += '&name=' + this.empName
  984. }
  985. // 17、添加分页参数 ?currentPage='+this.currentPage+'&size='+this.size
  986. // 19、添加用户名搜索参数 +'&name='+this.empName,传参 根据条件搜索,不传参查询所有
  987. this.getRequest(url).then(resp => {
  988. // this.getRequest('/employee/basic/').then(resp => {
  989. this.loading = false // 9、关闭 loading
  990. if (resp) {
  991. this.emps = resp.data
  992. this.total = resp.total // 12、分页
  993. }
  994. });
  995. }
  996. }
  997. }
  998. </script>
  999. <style>
  1000. /*28-7 展开收起条件搜索框动画样式 */
  1001. /* 可以设置不同的进入和离开动画 */
  1002. /* 设置持续时间和动画函数 */
  1003. .slide-fade-enter-active {
  1004. transition: all .8s ease;
  1005. }
  1006. .slide-fade-leave-active {
  1007. transition: all .8s cubic-bezier(1.0, 0.5, 0.8, 1.0);
  1008. }
  1009. .slide-fade-enter, .slide-fade-leave-to
  1010. /* .slide-fade-leave-active for below version 2.1.8 */
  1011. {
  1012. transform: translateX(10px);
  1013. opacity: 0;
  1014. }
  1015. </style>

十、工资账套管理

树形结构

 SalSob.vue

  1. <template>
  2. <div>
  3. <!-- 1-1 绘制表格 -->
  4. <div style="display: flex;justify-content: space-between;">
  5. <!-- 2-3 @click="showAddSalaryView" 点击打开 添加工资账套对话框 -->
  6. <el-button type="primary" icon="el-icon-plus" size="mini" @click="showAddSalaryView">添加工资账套</el-button>
  7. <!-- 5-3 刷新功能 直接绑定点击事件 调用获取所有数据方法 -->
  8. <el-button type="success" icon="el-icon-refresh" size="mini" @click="initSalaries"></el-button>
  9. </div>
  10. <div style="margin-top: 10px;">
  11. <el-table
  12. :data="salaries"
  13. stripe
  14. border>
  15. <!-- 多选框 type="selection" -->
  16. <el-table-column
  17. type="selection"
  18. width="40">
  19. </el-table-column>
  20. <el-table-column
  21. prop="name"
  22. label="账套名称"
  23. width="120">
  24. </el-table-column>
  25. <el-table-column
  26. prop="basicSalary"
  27. label="基本工资"
  28. width="70">
  29. </el-table-column>
  30. <el-table-column
  31. prop="trafficSalary"
  32. label="交通补助"
  33. width="70">
  34. </el-table-column>
  35. <el-table-column
  36. prop="lunchSalary"
  37. label="午餐补助"
  38. width="70">
  39. </el-table-column>
  40. <el-table-column
  41. prop="bonus"
  42. label="奖金"
  43. width="70">
  44. </el-table-column>
  45. <el-table-column
  46. prop="createDate"
  47. label="启用时间"
  48. width="100">
  49. </el-table-column>
  50. <!-- 多级表头:el-table-column 里面嵌套 el-table-column,就可以实现多级表头 -->
  51. <el-table-column
  52. label="养老金"
  53. align="center">
  54. <el-table-column
  55. prop="pensionPer"
  56. label="比率"
  57. width="70">
  58. </el-table-column>
  59. <el-table-column
  60. prop="pensionBase"
  61. label="基数"
  62. width="70">
  63. </el-table-column>
  64. </el-table-column>
  65. <!-- 多级表头 -->
  66. <el-table-column
  67. label="医疗保险"
  68. align="center">
  69. <el-table-column
  70. prop="medicalPer"
  71. label="比率"
  72. width="70">
  73. </el-table-column>
  74. <el-table-column
  75. prop="medicalBase"
  76. label="基数"
  77. width="70">
  78. </el-table-column>
  79. </el-table-column>
  80. <!-- 多级表头 -->
  81. <el-table-column
  82. label="公积金"
  83. align="center">
  84. <el-table-column
  85. prop="accumulationFundPer"
  86. label="比率"
  87. width="70">
  88. </el-table-column>
  89. <el-table-column
  90. prop="accumulationFundBase"
  91. label="基数"
  92. width="70">
  93. </el-table-column>
  94. </el-table-column>
  95. <el-table-column
  96. label="操作">
  97. <!-- 5-1 删除工资账套 拿到当前行数据 绑定点击事件 传行数据-->
  98. <template slot-scope="scope">
  99. <!-- 6-4 @click="showEditSalaryView(scope.row)">编辑 -->
  100. <el-button type="primary" size="mini" @click="showEditSalaryView(scope.row)">编辑</el-button>
  101. <el-button type="danger" size="mini" @click="deleteSalary(scope.row)">删除</el-button>
  102. </template>
  103. </el-table-column>
  104. </el-table>
  105. </div>
  106. <!-- 2-1 添加工资账套对话框 -->
  107. <!-- 6-2 把标题变成属性 -->
  108. <el-dialog
  109. :title="dialogTitle"
  110. :visible.sync="dialogVisible"
  111. width="50%">
  112. <!-- 3-8 调整样式 -->
  113. <div style="display: flex;justify-content: space-around;align-items: center;">
  114. <!-- 3-1 添加步骤条 -->
  115. <!-- 3-5 :active="activeItemIndex" -->
  116. <el-steps direction="vertical" :active="activeItemIndex">
  117. <!-- 3-3 循环遍历数据 -->
  118. <el-step :title="itemName" v-for="(itemName,index) in salaryItemName" :key="index"></el-step>
  119. </el-steps>
  120. <!-- 3-4 循环遍历数据 -->
  121. <!-- 3-7 v-show="activeItemIndex = index" 与下标相等才展示 -->
  122. <!-- 4-2 修改各项的值 绑定和遍历-->
  123. <el-input v-model="salary[title]" :placeholder="'请输入'+salaryItemName[index]+'...'"
  124. v-for="(value,title,index) in salary"
  125. :key="index" v-show="activeItemIndex === index" style="width: 200px;"></el-input>
  126. </div>
  127. <span slot="footer" class="dialog-footer">
  128. <!-- 3-10 按钮判断根据索引显示 文字提示 -->
  129. <el-button @click="preStep">{{ activeItemIndex === 10 ? '取消' : '上一步' }}</el-button>
  130. <el-button type="primary" @click="nextStep">{{ activeItemIndex === 10 ? '完成' : '下一步' }}</el-button>
  131. </span>
  132. </el-dialog>
  133. </div>
  134. </template>
  135. <script>
  136. export default {
  137. name: "SalSob",
  138. data() {
  139. return {
  140. dialogTitle: '添加工资账套', // 6-1 标题
  141. dialogVisible: false, // 2-2 添加工资账套对话框
  142. salaries: [], // 1-2 定义数组
  143. activeItemIndex: 0, // 3-6 步骤条激活索引
  144. salaryItemName: [ // 3-2 步骤条数据对象
  145. '账套名称',
  146. '基本工资',
  147. '交通补助',
  148. '午餐补助',
  149. '奖金',
  150. '养老金比率',
  151. '养老金基数',
  152. '医疗保险比率',
  153. '医疗保险基数',
  154. '公积金比率',
  155. '公积金基数'
  156. ],
  157. // 4-1 定义工资账套数据
  158. salary: {
  159. name: '',
  160. basicSalary: 0,
  161. trafficSalary: 0,
  162. lunchSalary: 0,
  163. bonus: 0,
  164. pensionPer: 0.0,
  165. pensionBase: 0,
  166. medicalPer: 0.0,
  167. medicalBase: 0,
  168. accumulationFundPer: 0.0,
  169. accumulationFundBase: 0
  170. }
  171. }
  172. },
  173. mounted() {
  174. this.initSalaries()
  175. },
  176. methods: {
  177. // 6-5 点击编辑显示对话框
  178. showEditSalaryView(data) {
  179. this.dialogTitle = '编辑工资账套' // 设置标题
  180. this.activeItemIndex = 0 // 默认激活的索引
  181. this.salary.id = data.id
  182. this.salary.name = data.name
  183. this.salary.basicSalary = data.basicSalary
  184. this.salary.trafficSalary = data.trafficSalary
  185. this.salary.lunchSalary = data.lunchSalary
  186. this.salary.bonus = data.bonus
  187. this.salary.pensionPer = data.pensionPer
  188. this.salary.pensionBase = data.pensionBase
  189. this.salary.medicalPer = data.medicalPer
  190. this.salary.medicalBase = data.medicalBase
  191. this.salary.accumulationFundPer = data.accumulationFundPer
  192. this.salary.accumulationFundBase = data.accumulationFundBase
  193. this.dialogVisible = true // 打开对话框
  194. },
  195. // 5-2 删除工资账套
  196. deleteSalary(data) {
  197. this.$confirm('此操作将永久删除该[' + data.name + ']工资账套, 是否继续?', '提示', {
  198. confirmButtonText: '确定',
  199. cancelButtonText: '取消',
  200. type: 'warning'
  201. }).then(() => {
  202. this.deleteRequest('/salary/sob/' + data.id).then(resp => {
  203. if (resp) {
  204. this.initSalaries()
  205. }
  206. })
  207. }).catch(() => {
  208. this.$message({
  209. type: 'info',
  210. message: '已取消删除'
  211. });
  212. });
  213. },
  214. preStep() { // 3-13 上一步 取消
  215. if (this.activeItemIndex === 0) {
  216. return
  217. } else if (this.activeItemIndex === 10) {
  218. this.dialogVisible = false;
  219. return;
  220. }
  221. this.activeItemIndex--
  222. },
  223. nextStep() { // 3-12 下一步 完成
  224. if (this.activeItemIndex === 10) {
  225. // alert("ok")
  226. // console.log(this.salary)
  227. // 4-4 添加工资账套
  228. if (this.salary.id) { // 6-6 有 id 调用编辑接口,没有 id 执行添加
  229. this.putRequest('/salary/sob/', this.salary).then(resp => {
  230. if (resp) {
  231. this.initSalaries()
  232. this.dialogVisible = false // 关闭弹框
  233. }
  234. })
  235. } else {
  236. this.postRequest('/salary/sob/', this.salary).then(resp => {
  237. if (resp) {
  238. this.initSalaries()
  239. this.dialogVisible = false
  240. }
  241. })
  242. }
  243. return
  244. }
  245. this.activeItemIndex++
  246. },
  247. // 2-4 点击打开添加工资账套对话框
  248. showAddSalaryView() {
  249. this.dialogTitle = '添加工资账套' // 6-3 添加的时候显示此标题
  250. this.salary = { // 4-3 清空表单
  251. name: '',
  252. basicSalary: 0,
  253. trafficSalary: 0,
  254. lunchSalary: 0,
  255. bonus: 0,
  256. pensionPer: 0.0,
  257. pensionBase: 0,
  258. medicalPer: 0.0,
  259. medicalBase: 0,
  260. accumulationFundPer: 0.0,
  261. accumulationFundBase: 0
  262. }
  263. this.activeItemIndex = 0 // 3-14 步骤条索引从0开始
  264. this.dialogVisible = true;
  265. },
  266. // 1-3 初始化数据
  267. initSalaries() {
  268. this.getRequest('/salary/sob/').then(resp => {
  269. if (resp) {
  270. this.salaries = resp
  271. }
  272. })
  273. }
  274. }
  275. }
  276. </script>
  277. <style scoped>
  278. </style>

十一、员工账套设置

SalSobCfg.vue

  1. <template>
  2. <div>
  3. <el-table
  4. size="mini"
  5. :data="emps"
  6. stripe
  7. border>
  8. <el-table-column
  9. align="left"
  10. type="selection"
  11. width="55">
  12. </el-table-column>
  13. <el-table-column
  14. prop="name"
  15. label="姓名"
  16. align="left"
  17. fixed
  18. width="120">
  19. </el-table-column>
  20. <el-table-column
  21. prop="workId"
  22. label="工号"
  23. align="left"
  24. width="120">
  25. </el-table-column>
  26. <el-table-column
  27. prop="email"
  28. label="邮箱地址"
  29. align="left"
  30. width="200">
  31. </el-table-column>
  32. <el-table-column
  33. prop="phone"
  34. label="电话号码"
  35. align="left"
  36. width="120">
  37. </el-table-column>
  38. <el-table-column
  39. prop="department.name"
  40. label="所属部门"
  41. align="left"
  42. width="120">
  43. </el-table-column>
  44. <el-table-column
  45. label="工资账套"
  46. align="center">
  47. <template slot-scope="scope">
  48. <el-tooltip placement="right" v-if="scope.row.salary">
  49. <div slot="content">
  50. <table>
  51. <tr>
  52. <td>基本工资</td>
  53. <td>{{ scope.row.salary.basicSalary }}</td>
  54. </tr>
  55. <tr>
  56. <td>交通补助</td>
  57. <td>{{ scope.row.salary.trafficSalary }}</td>
  58. </tr>
  59. <tr>
  60. <td>午餐补助</td>
  61. <td>{{ scope.row.salary.lunchSalary }}</td>
  62. </tr>
  63. <tr>
  64. <td>奖金</td>
  65. <td>{{ scope.row.salary.bonus }}</td>
  66. </tr>
  67. <tr>
  68. <td>养老金比率</td>
  69. <td>{{ scope.row.salary.pensionPer }}</td>
  70. </tr>
  71. <tr>
  72. <td>养老金基数</td>
  73. <td>{{ scope.row.salary.pensionBase }}</td>
  74. </tr>
  75. <tr>
  76. <td>医疗保险比率</td>
  77. <td>{{ scope.row.salary.medicalPer }}</td>
  78. </tr>
  79. <tr>
  80. <td>医疗保险基数</td>
  81. <td>{{ scope.row.salary.medicalBase }}</td>
  82. </tr>
  83. <tr>
  84. <td>公积金比率</td>
  85. <td>{{ scope.row.salary.accumulationFundPer }}</td>
  86. </tr>
  87. <tr>
  88. <td>公积金基数</td>
  89. <td>{{ scope.row.salary.accumulationFundBase }}</td>
  90. </tr>
  91. </table>
  92. </div>
  93. <el-tag>{{ scope.row.salary.name }}</el-tag>
  94. </el-tooltip>
  95. <el-tag v-else>暂未设置</el-tag>
  96. </template>
  97. </el-table-column>
  98. <!-- 2-1 编辑工资账套 -->
  99. <el-table-column
  100. label="操作"
  101. align="center">
  102. <template slot-scope="scope">
  103. <!-- 2-5 当前员工的工资账套 @show="showPop(scope.row.salary)" show 显示时触发 -->
  104. <!-- 2-9 @hide="hidePop(scope.row)" hide 隐藏时触发 -->
  105. <el-popover
  106. size="mini"
  107. @show="showPop(scope.row.salary)"
  108. @hide="hidePop(scope.row)"
  109. placement="right"
  110. title="编辑工资账套"
  111. width="200"
  112. trigger="click">
  113. <div>
  114. <!-- 2-6 v-model="currentSalary" -->
  115. <el-select v-model="currentSalary" placeholder="请选择">
  116. <el-option
  117. size="mini"
  118. v-for="item in salaries"
  119. :key="item.id"
  120. :label="item.name"
  121. :value="item.id">
  122. </el-option>
  123. </el-select>
  124. </div>
  125. <el-button slot="reference" type="danger">修改工资账套</el-button>
  126. </el-popover>
  127. </template>
  128. </el-table-column>
  129. </el-table>
  130. <!-- 1-1 分页组件 -->
  131. <div style="display: flex;justify-content: flex-end;margin-top: 5px;">
  132. <el-pagination
  133. @size-change="sizeChange"
  134. @current-change="currentChange"
  135. layout="total, sizes, prev, pager, next, jumper"
  136. :total="total" background>
  137. </el-pagination>
  138. </div>
  139. </div>
  140. </template>
  141. <script>
  142. export default {
  143. name: "SalSobCfg",
  144. data() {
  145. return {
  146. emps: [],
  147. salaries: [], // 2-2 工资账套数组
  148. currentPage: 1, // 1-2 当前页
  149. size: 10, // 1-2 每页显示条数
  150. total: 0, // 1-2 分页
  151. currentSalary: null // 2-7 当前员工工资账套
  152. }
  153. },
  154. mounted() {
  155. this.initEmps()
  156. this.initSalaries() // 2-4 初始化 获取所有工资账套
  157. },
  158. methods: {
  159. // 2-10
  160. hidePop(data) { // 隐藏时触发
  161. // 当前员工工资账套存在 并且不等于当前的 才更新
  162. if (this.currentSalary && this.currentSalary!==data.salary.id) {
  163. this.putRequest('/salary/sobcfg/?eid=' + data.id + '&sid=' + this.currentSalary).then(resp => {
  164. if (resp) {
  165. this.initEmps()
  166. }
  167. });
  168. }
  169. },
  170. // 2-8 员工工资账套
  171. showPop(data) { // 显示时触发
  172. if (data) {
  173. this.currentSalary = data.id;
  174. } else {
  175. this.currentSalary = null
  176. }
  177. },
  178. // 2-3 获取所有工资账套
  179. initSalaries() {
  180. this.getRequest('/salary/sobcfg/salaries').then(resp => {
  181. if (resp) {
  182. this.salaries = resp
  183. }
  184. })
  185. },
  186. // 1-3 分页-当前页
  187. currentChange(page) {
  188. this.currentPage = page
  189. this.initEmps()
  190. },
  191. // 1-4 分页-每页显示数量
  192. sizeChange(size) {
  193. this.size = size
  194. this.initEmps()
  195. },
  196. // 获取所有数据
  197. initEmps() {
  198. this.getRequest('/salary/sobcfg/?currentPage=' + this.currentPage + '&size=' + this.size).then(resp => {
  199. if (resp) {
  200. this.emps = resp.data
  201. this.total = resp.total
  202. }
  203. })
  204. }
  205. }
  206. }
  207. </script>
  208. <style scoped>
  209. </style>

十二、聊天功能

安装npm install --save stompjs

将GitHub上的开源项目与自身项目进行整合来实现即时聊天功能

简介

一个基于Vue + Webpack构建的简单chat示例,聊天记录保存在localStorge。简单演示了Vue的 component、filter、directive、computed以及组件间的事件通讯。 原项目目前存在一个Bug:打开项目关闭浏览器再次打开会报错。这里使用在此项目基础上重构的项目 来与我们项目进行整合.

下载(用chrom打开下载)

地址:

整合项目

将下载下来的项目所在的文件,加入到本项目中。

在这里插入图片描述

assets:是网页所需要的图片,这个不需要,因为后端提供了图片,更改代码直接从后端获取。

componts:在自己的目录下创建该目录下的几个文件,复制过去。

vuex:是stroy,将其中的的代码,加入到本项目的story/index.js

main.js:就是一个普通的入口,不需要加入项目当汇总。

components

card.vue

  1. <template>
  2. <div id="card">
  3. <header>
  4. <img class="avatar" v-bind:src="user.userFace" v-bind:alt="user.name">
  5. <p class="name">{{user.name}}</p>
  6. </header>
  7. <footer>
  8. <input class="search" type="text" v-model="$store.state.filterKey" placeholder="search user...">
  9. </footer>
  10. </div>
  11. </template>
  12. <script>
  13. export default {
  14. name: 'card',
  15. data () {
  16. return {
  17. user:JSON.parse(window.sessionStorage.getItem("user"))
  18. }
  19. }
  20. }
  21. </script>
  22. <style lang="scss" scoped>
  23. #card {
  24. padding: 12px;
  25. .avatar{
  26. width: 40px;
  27. height: 40px;
  28. vertical-align: middle;/*这个是图片和文字居中对齐*/
  29. }
  30. .name {
  31. display: inline-block;
  32. padding: 10px;
  33. margin-bottom: 15px;
  34. font-size: 16px;
  35. }
  36. .search {
  37. background: #26292E;
  38. height: 30px;
  39. line-height: 30px;
  40. padding: 0 10px;
  41. border: 1px solid #3a3a3a;
  42. border-radius: 4px;
  43. outline: none;/*鼠标点击后不会出现蓝色边框*/
  44. color: #FFF;
  45. }
  46. }
  47. </style>

list.vue

  1. <template>
  2. <div id="list">
  3. <ul style="padding-left: 0;">
  4. <li v-for="item in admins" :class="{ active: currentSession?item.username === currentSession.username:false }"
  5. v-on:click="changecurrentSession(item)"><!-- :class="[item.id === currentSession ? 'active':'']" -->
  6. <!-- 未读消息提示 小红点 <el-badge is-dot> </el-badge> -->
  7. <el-badge is-dot :is-dot="idDot[user.username+'#'+item.username]"><img class="avatar" :src="item.userFace" :alt="item.name"></el-badge>
  8. <p class="name">{{ item.name }}</p>
  9. </li>
  10. </ul>
  11. </div>
  12. </template>
  13. <script>
  14. import {mapState} from 'vuex'
  15. export default {
  16. name: 'list',
  17. data() {
  18. return {
  19. user:JSON.parse(window.sessionStorage.getItem('user'))
  20. }
  21. },
  22. computed: mapState([
  23. 'idDot',
  24. 'admins',
  25. 'currentSession'
  26. ]),
  27. methods: {
  28. changecurrentSession: function (currentSession) {
  29. this.$store.commit('changecurrentSession', currentSession)
  30. }
  31. }
  32. }
  33. </script>
  34. <style lang="scss" scoped>
  35. #list {
  36. li {
  37. padding: 0 15px;
  38. border-bottom: 1px solid #292C33;
  39. cursor: pointer;
  40. &:hover {
  41. background-color: rgba(255, 255, 255, 0.03);
  42. }
  43. }
  44. li.active { /*注意这个是.不是冒号:*/
  45. background-color: rgba(255, 255, 255, 0.1);
  46. }
  47. .avatar {
  48. border-radius: 2px;
  49. width: 30px;
  50. height: 30px;
  51. vertical-align: middle;
  52. }
  53. .name {
  54. display: inline-block;
  55. margin-left: 15px;
  56. }
  57. }
  58. </style>

message.vue

  1. <template>
  2. <div id="message" v-scroll-bottom="sessions">
  3. <ul v-if="currentSession">
  4. <li v-for="entry in sessions[user.username+'#'+currentSession.username]">
  5. <p class="time">
  6. <span>{{ entry.date | time }}</span>
  7. </p>
  8. <div class="main" :class="{self:entry.self}">
  9. <img class="avatar" :src="entry.self ? user.userFace:currentSession.userFace" alt="">
  10. <p class="text">{{ entry.content }}</p>
  11. </div>
  12. </li>
  13. </ul>
  14. </div>
  15. </template>
  16. <script>
  17. import {mapState} from 'vuex'
  18. export default {
  19. name: 'message',
  20. data() {
  21. return {
  22. user: JSON.parse(window.sessionStorage.getItem('user')), // 当前用户
  23. }
  24. },
  25. computed: mapState([
  26. 'sessions',
  27. 'currentSession'
  28. ]),
  29. filters: {
  30. time(date) {
  31. if (date) {
  32. date = new Date(date);
  33. }
  34. return `${date.getHours()}:${date.getMinutes()}`;
  35. }
  36. },
  37. directives: {/*这个是vue的自定义指令,官方文档有详细说明*/
  38. // 发送消息后滚动到底部,这里无法使用原作者的方法,也未找到合理的方法解决,暂用setTimeout的方法模拟
  39. 'scroll-bottom'(el) {
  40. //console.log(el.scrollTop);
  41. setTimeout(function () {
  42. el.scrollTop += 9999;
  43. }, 1)
  44. }
  45. }
  46. }
  47. </script>
  48. <style lang="scss" scoped>
  49. #message {
  50. padding: 15px;
  51. max-height: 68%;
  52. overflow-y: scroll;
  53. ul {
  54. list-style-type: none;
  55. padding-left: 0;
  56. li {
  57. margin-bottom: 15px;
  58. }
  59. }
  60. .time {
  61. text-align: center;
  62. margin: 7px 0;
  63. > span {
  64. display: inline-block;
  65. padding: 0 18px;
  66. font-size: 12px;
  67. color: #FFF;
  68. background-color: #dcdcdc;
  69. border-radius: 2px;
  70. }
  71. }
  72. .main {
  73. .avatar {
  74. float: left;
  75. margin: 0 10px 0 0;
  76. border-radius: 3px;
  77. width: 30px;
  78. height: 30px;
  79. }
  80. .text {
  81. display: inline-block;
  82. padding: 0 10px;
  83. max-width: 80%;
  84. background-color: #fafafa;
  85. border-radius: 4px;
  86. line-height: 30px;
  87. }
  88. }
  89. .self {
  90. text-align: right;
  91. .avatar {
  92. float: right;
  93. margin: 0 0 0 10px;
  94. border-radius: 3px;
  95. width: 30px;
  96. height: 30px;
  97. }
  98. .text {
  99. display: inline-block;
  100. padding: 0 10px;
  101. max-width: 80%;
  102. background-color: #b2e281;
  103. border-radius: 4px;
  104. line-height: 30px;
  105. }
  106. }
  107. }
  108. </style>

usertext.vue

  1. <template>
  2. <div id="uesrtext">
  3. <textarea placeholder="按 Ctrl + Enter 发送" v-model="content" v-on:keyup="addMessage"></textarea>
  4. </div>
  5. </template>
  6. <script>
  7. import {mapState} from 'vuex'
  8. export default {
  9. name: 'uesrtext',
  10. data() {
  11. return {
  12. content: ''
  13. }
  14. },
  15. computed: mapState([
  16. 'currentSession'
  17. ]),
  18. methods: {
  19. addMessage(e) {
  20. if (e.ctrlKey && e.keyCode === 13 && this.content.length) {
  21. // 自定义发送消息
  22. let msgObj = {}
  23. // let msgObj = new Object()
  24. msgObj.to = this.currentSession.username
  25. msgObj.content = this.content
  26. this.$store.state.stomp.send('/ws/chat', {}, JSON.stringify(msgObj))
  27. this.$store.commit('addMessage', msgObj);
  28. this.content = '';
  29. }
  30. }
  31. }
  32. }
  33. </script>
  34. <style lang="scss" scoped>
  35. #uesrtext {
  36. position: absolute;
  37. bottom: 0;
  38. right: 0;
  39. width: 100%;
  40. height: 30%;
  41. border-top: solid 1px #DDD;
  42. > textarea {
  43. padding: 10px;
  44. width: 100%;
  45. height: 100%;
  46. border: none;
  47. outline: none;
  48. }
  49. }
  50. </style>
  51. chat/FriendChat.vue
  52. <template>
  53. <div id="app">
  54. <div class="sidebar">
  55. <card></card>
  56. <list></list>
  57. </div>
  58. <div class="main">
  59. <message></message>
  60. <userText></userText>
  61. </div>
  62. </div>
  63. </template>
  64. <script>
  65. import card from '@/components/chat/card.vue'
  66. import list from '@/components/chat/list.vue'
  67. import message from '@/components/chat/message.vue'
  68. import userText from '@/components/chat/usertext.vue'
  69. export default {
  70. name: 'FriendChat',
  71. data () {
  72. return {
  73. }
  74. },
  75. mounted:function() {
  76. this.$store.dispatch('initData');
  77. },
  78. components:{
  79. card,
  80. list,
  81. message,
  82. userText
  83. }
  84. }
  85. </script>
  86. <style lang="scss" scoped>
  87. #app {
  88. margin: 20px 100px;
  89. //margin: 20px auto;
  90. width: 800px;
  91. height: 600px;
  92. overflow: hidden;
  93. border-radius: 10px;
  94. border: 1px solid #c8c9c9;
  95. .sidebar, .main {
  96. height: 100%;
  97. }
  98. .sidebar {
  99. float: left;
  100. color: #f4f4f4;
  101. background-color: #2e3238;
  102. width: 200px;
  103. }
  104. .main {
  105. position: relative;
  106. overflow: hidden;
  107. background-color: #eee;
  108. }
  109. }
  110. </style>

十三、个人中心

 AdminInfo.vue

  1. <template>
  2. <div>
  3. <el-card class="box-card" style="width: 400px;">
  4. <div slot="header" class="clearfix">
  5. <span>{{ admin.name }}</span>
  6. </div>
  7. <div>
  8. <div>
  9. <div style="display: flex;justify-content: center;">
  10. <img title="点击修改用户头像" :src="admin.userFace" style="height: 100px;width: 100px;border-radius: 50px;" alt="">
  11. </div>
  12. <div>电话号码:
  13. <el-tag>{{ admin.telephone }}</el-tag>
  14. </div>
  15. <div>手机号码:
  16. <el-tag>{{ admin.phone }}</el-tag>
  17. </div>
  18. <div>居住地址:
  19. <el-tag>{{ admin.address }}</el-tag>
  20. </div>
  21. <div>用户标签:
  22. <el-tag type="success" v-for="(r,index) in admin.roles" :key="index">{{ r.nameZh }}</el-tag>
  23. </div>
  24. </div>
  25. <div style="display: flex;justify-content: space-around;margin-top: 10px;">
  26. <!-- 1-3 @click="showUpdateAdminInfoView" -->
  27. <el-button type="primary" size="mini" @click="showUpdateAdminInfoView">修改信息</el-button>
  28. <!-- 2-1 用户修改密码 @click="showUpdatePasswordView" -->
  29. <el-button type="danger" size="mini" @click="showUpdatePasswordView">修改密码</el-button>
  30. </div>
  31. </div>
  32. </el-card>
  33. <!-- 1-1 编辑用户信息 -->
  34. <el-dialog
  35. title="编辑用户信息"
  36. :visible.sync="dialogVisible"
  37. width="30%">
  38. <div>
  39. <table>
  40. <tr>
  41. <td>用户昵称:</td>
  42. <td>
  43. <!-- 1-5 重新给每项赋值 admin2 -->
  44. <el-input v-model="admin2.name"></el-input>
  45. </td>
  46. </tr>
  47. <tr>
  48. <td>电话号码:</td>
  49. <td>
  50. <el-input v-model="admin2.telephone"></el-input>
  51. </td>
  52. </tr>
  53. <tr>
  54. <td>手机号码:</td>
  55. <td>
  56. <el-input v-model="admin2.phone"></el-input>
  57. </td>
  58. </tr>
  59. <tr>
  60. <td>用户地址:</td>
  61. <td>
  62. <el-input v-model="admin2.address"></el-input>
  63. </td>
  64. </tr>
  65. </table>
  66. </div>
  67. <span slot="footer" class="dialog-footer">
  68. <el-button @click="dialogVisible = false">取 消</el-button>
  69. <!-- 1-8 @click="updateAdminInfo" -->
  70. <el-button type="primary" @click="updateAdminInfo">确 定</el-button>
  71. </span>
  72. </el-dialog>
  73. <!-- 2-2 修改密码 -->
  74. <el-dialog
  75. title="更新密码"
  76. :visible.sync="passwordDialogVisible"
  77. width="30%">
  78. <div>
  79. <!-- 2-8 调整修改密码表单 -->
  80. <el-form :model="ruleForm" status-icon :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
  81. <el-form-item label="请输入旧密码" prop="oldPass">
  82. <el-input type="password" v-model="ruleForm.oldPass" autocomplete="off"></el-input>
  83. </el-form-item>
  84. <el-form-item label="请输入新密码" prop="pass">
  85. <el-input type="password" v-model="ruleForm.pass" autocomplete="off"></el-input>
  86. </el-form-item>
  87. <el-form-item label="确认新密码" prop="checkPass">
  88. <el-input type="password" v-model="ruleForm.checkPass" autocomplete="off"></el-input>
  89. </el-form-item>
  90. <el-form-item>
  91. <el-button type="primary" @click="submitForm('ruleForm')">提交</el-button>
  92. <el-button @click="resetForm('ruleForm')">重置</el-button>
  93. </el-form-item>
  94. </el-form>
  95. </div>
  96. </el-dialog>
  97. </div>
  98. </template>
  99. <script>
  100. export default {
  101. name: "AdminInfo",
  102. data() {
  103. // 2-5 修改密码校验规则 一定要放最前面
  104. var validatePass = (rule, value, callback) => {
  105. if (value === '') {
  106. callback(new Error('请输入密码'));
  107. } else {
  108. if (this.ruleForm.checkPass !== '') {
  109. this.$refs.ruleForm.validateField('checkPass');
  110. }
  111. callback();
  112. }
  113. }
  114. var validatePass2 = (rule, value, callback) => {
  115. if (value === '') {
  116. callback(new Error('请再次输入密码'));
  117. } else if (value !== this.ruleForm.pass) {
  118. callback(new Error('两次输入密码不一致!'));
  119. } else {
  120. callback();
  121. }
  122. }
  123. return {
  124. admin: null,
  125. admin2: null, // 1-5 编辑的对象
  126. dialogVisible: false, // 1-2 编辑用户信息
  127. passwordDialogVisible: false, // 2-3 修改密码
  128. ruleForm: { // 2-6 校验对象 规则
  129. pass: '',
  130. checkPass: '',
  131. oldPass: '', // 2-9
  132. },
  133. rules: {
  134. pass: [
  135. {validator: validatePass, trigger: 'blur'}
  136. ],
  137. checkPass: [
  138. {validator: validatePass2, trigger: 'blur'}
  139. ],
  140. oldPass: [
  141. {validator: validatePass, trigger: 'blur'}
  142. ]
  143. }
  144. }
  145. },
  146. mounted() {
  147. this.initAdmin()
  148. },
  149. methods: {
  150. // 2-7 预校验 提交表单
  151. submitForm(formName) {
  152. this.$refs[formName].validate((valid) => {
  153. if (valid) {
  154. // alert('submit!');
  155. this.ruleForm.adminId = this.admin.id
  156. this.putRequest('/admin/pass', this.ruleForm).then(resp => {
  157. if (resp) {
  158. // 更新密码成功后 退出登录
  159. this.postRequest('/logout') // 退出登录
  160. window.sessionStorage.removeItem('user')
  161. window.sessionStorage.removeItem('tokenStr')
  162. this.$store.commit('initRoutes', []) // 初始化路由 菜单 置空
  163. this.$router.replace('/') // 跳到登录页面
  164. }
  165. })
  166. } else {
  167. console.log('error submit!!');
  168. return false;
  169. }
  170. });
  171. },
  172. // 2-7 重围修改密码表单
  173. resetForm(formName) {
  174. this.$refs[formName].resetFields();
  175. },
  176. // 2-4 修改密码
  177. showUpdatePasswordView() {
  178. this.passwordDialogVisible = true
  179. },
  180. // 1-9 更新用户
  181. updateAdminInfo() {
  182. this.putRequest('/admin/info', this.admin2).then(resp => {
  183. if (resp) {
  184. this.dialogVisible = false
  185. this.initAdmin()
  186. }
  187. })
  188. },
  189. // 1-4 编辑用户信息弹框
  190. showUpdateAdminInfoView() {
  191. this.dialogVisible = true
  192. },
  193. initAdmin() {
  194. this.getRequest('/admin/info').then(resp => {
  195. if (resp) {
  196. this.admin = resp
  197. this.admin2 = Object.assign({}, this.admin) // 1-6 对象拷贝给 admin2
  198. window.sessionStorage.setItem('user', JSON.stringify(resp))
  199. this.$store.commit('INIT_ADMIN', resp)
  200. }
  201. })
  202. }
  203. }
  204. }
  205. </script>
  206. <style scoped>
  207. </style>

十四、源代码

前端地址:

后端地址:

网站建设定制开发 软件系统开发定制 定制软件开发 软件开发定制 定制app开发 app开发定制 app开发定制公司 电商商城定制开发 定制小程序开发 定制开发小程序 客户管理系统开发定制 定制网站 定制开发 crm开发定制 开发公司 小程序开发定制 定制软件 收款定制开发 企业网站定制开发 定制化开发 android系统定制开发 定制小程序开发费用 定制设计 专注app软件定制开发 软件开发定制定制 知名网站建设定制 软件定制开发供应商 应用系统定制开发 软件系统定制开发 企业管理系统定制开发 系统定制开发