一、项目简介
项目背景:受疫情的影响,许多企业由线上办公转为线下办公。随着线上办公的人数的增多,线上办公的优点逐步凸显:通过实现工作流程的自动化、节省企业办公费用、实现绿色办公,同时提升办公效率。
项目介绍:本项目实现了一个在线办公系统,用来管理日常办公事物的:日常流程审批,新闻,通知,公告,文件信息,财务,人事,费用,资产,行政,项目,移动办公等。通过软件的方式让办公系统根据方便管理,提高整体的管理运营水平。
实现方式:本项目基于Vue+Spring Boot构架一个前后端分离项目。前端使用社区非常活跃的开源框架vue进行构建。简单地说,前后端分离 的核心思想是前端页面通过 ajax 调用后端的 restuful api 进行数据交互,而 单页面应用(single page web application,SPA),就是只有一张页面,并在用户与应用程序交互时动态更新该页面的 Web 应用程序。
1.1 技术架构
1.2 前端技术架构
本项目采用前后端分离开发模式,使用Spring Boot构建后端。
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;文件上传下载:js-file-download;在线聊天开源项目:vue-chat
前端模块分为:登录、职位管理、职称管理、部门管理、操作员历、员工管理、工资账套管理、个人中心、在线聊天
1.3 云E办(前端)
前后端之间通过 RESTful API 传递 JSON 数据进行交流。不同于 JSP 之类,后端是不涉及页面本身的内容的。在开发的时候,前端用前端的服务器(Nginx),后端用后端的服务器(Tomcat),当我开发前端内容的时候,可以把前端的请求通过前端服务器转发给后端(称为反向代理),这样就能实时观察结果,并且不需要知道后端怎么实现,而只需要知道接口提供的功能。
目录
二、Vue.js框架
Web前端开发从开始到兴起再到如今,已经发展了很多年,同时累积了非常多的开发经验和开发工具。过去的开发者们的一些经验也都经过了技术发展,环境变化等重重考验,他们所创造出来的思想、技术和工具,也非常值得我们后来开发者的借鉴和学习,甚至直接拿来使用。因为开发工具和开发语言不管怎么发展,不管差异有多大,但是他们所解决的问题都是相似而统一的,可以归纳为:
(1)扩充JavaScript、html和CSS三种编程语言本身的语言能力;
为了解决上述问题便产生了工程化的思想,工程化就是这种避免重复造轮子的最好实践。Vue.js是一套构建用户界面的渐进式框架,它是由中国程序开发者尤雨溪在2013年开发。因为Vue.js简洁的语法设计、轻量快速的特点在技术社区中深受开发者欢迎,因而也促进了Vue.js的推广和流行。Vue.js在相关的工具和支持库配合使用下,也能完美地驱动复杂的单页应用,开发一个大型的Web应用。Vue.js在权威的JavaScript趋势榜上已经蹿升到了总榜的前30位且能持续的排在榜单的前列,显然已成为一个全球顶尖的JavaScript框架。Vue.js的生态不仅体现在趋势榜上,其配套的数据管理库vuex、路由管理库Vue-router、打包工具、开发者调试插件和项目脚手架等工具和库也都逐渐开发成型,同样也拥有非常活跃的技术社区。
Vue.js是一个轻量级的MVVM前端框架,可以用于构建渐进式用户界面。开发者在Vue.js中构建前端页面时,只用关心页面逻辑的实现。Vue.js最大的特点是由底层逐层向上应用,不仅易上手还能兼容大量的第三方库。
2.1Vue特性
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.js框架进行快速开发。vue-cli将各种工具标准化,确保各种构建工具能够基于智能的默认配置平稳衔接,使开发者在撰写前端应用时更加专注,不必花费很长时间去调整项目配置。
Vue.js构建的单页面的Web应用需要基于路由和组件。其中,路由的主要作用是用来设定访问路径,并将访问路径和视图组件相映射起来。在单页面的Web应用里,路径之间的跳转和切换其实是对应组件之间的切换。和React.js一样,Vue.js自身是也是不具有路由功能的。因此,在Vue.js框架使用的时候需要与一个路由工具库相互协作,即Vue-router。Vue-router能够将不同层级并且嵌套的路由关系映射到相应的嵌套的组件,并且提供了一个细致的控制路径跳转的解决方式。
Vue.js的视图变化都基于组件的状态,所以当构建大型的Web应用的时候会产生大量的组件状态,从而需要对这些状态进行管理。由于vue的“单向数据流”无法解决多个组件共享状态的问题,所以才有了vuex。Vuex便是一个集中式存储和管理应用的所有组件的状态的管理架构,专门为配合使用了Vue.js框架的应用而设计。它借鉴了React.js的状态管理工具Flux和Redux的设计理念,并对一些概念进行了简化,从而能更好的发挥Vue.js的数据响应机制。
通信框架。因为vue的边界很明确,就是为了处理DOM,所以不具备通信能力,此时就需要额外使用一个通信框架与服务器交互;当然也可以直接使用jQuery提供的AJAX通信功能。经典的Ajax技术实现了网页的局部数据刷新,而Axios又对Ajax进行再次封装,它具备如下特征:
Axios插件很好的封装了Ajax技术,在项目开发中写法简洁明了,因此不容易出错,即使出错也易于排查。
前端开发和其他开发工作的主要区别,首先是前端基于多语言、多层次的编码和组织工作,其次前端产品的交付是基于浏览器的,这些资源是通过增量加载的方式运行到浏览器端,如何在开发环境组织好这些碎片化的代码和中资源,并且保证他们在浏览器快速、优雅的加载和更新,就需要一个模块化系统。
webpack是一个大型js应用程序的模块化工具,会自动处理命名空间等一系列js编程遇到的问题。当webpack处理js的应用程序时,它会构建一个复杂的依赖关系图,这个关系图中包含了应用程序依赖的模块,甚至包含了很多静态资源,然后webpack会将这些模块打包成一个或多个大的模块,在应用程序中引用。
vue通常用es6来写,用export default导出,其下面可以包含数据data,生命周期(mounted等),方法(methods)等,具体语法请看vue.js文档。ES6标准增加了javascript语言层面的模块体系定义。ES6模块的设计思想,是尽量静态化,使编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS和AMD模块,都只能在运行时确定这些东西。
三、搭建vue.js项目
3.1 搭建vue.js项目
安装Node.js(>=6.x,首选8.x)本项目是v14.18.0版本
因为需要使用 npm 安装 Vue CLI,而 npm 是集成在 Node.js 中的,所以第一步我们需要安装 Node.js,访问官网 Node.js,首页即可下载。
然后在 cmd 中输入 node -v,检查node是否安装成功。
输入 npm -g install npm ,将 npm 更新至最新版本。
之后,使用 npm install -g vue-cli 安装脚手架。(本项目使用版本2.9.6)
注意此种方式安装的是 2.x 版本的 Vue CLI,最新版本需要通过 npm install -g @vue/cli 安装。新版本可以使用图形化界面初始化项目,并加入了项目健康监控的内容,但使用新版本创建的项目依赖与这个教程不太相符,折腾起来比较麻烦。
或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 是我们构建单页面应用的关键。
可以看到 workspace 目录下生成了项目文件夹 需要在该文件夹执行 npm install ,npm run build 再执行 npm run dev
访问 http://localhost:8080,查看网页 demo,大工告成!
注:在vue项目中,有的时候需要执行npm run serve启动项目,有的时候需要用npm run dev,具体有什么不一样呢?
dev默认是vue[email protected]默认支持的命令;
serve默认是[email protected]及以上版本默认支持的命令。
3.3 vue项目结构分析
├── build --------------------------------- 项目构建(webpack)相关配置文件,配置参数什么的,一般不用动
│ ├── build.js --------------------------webpack打包配置文件
│ ├── check-versions.js ------------------------------ 检查npm,nodejs版本
│ ├── dev-client.js ---------------------------------- 设置环境
│ ├── dev-server.js ---------------------------------- 创建express服务器,配置中间件,启动可热重载的服务器,用于开发项目
│ ├── utils.js --------------------------------------- 配置资源路径,配置css加载器
│ ├── vue-loader.conf.js ----------------------------- 配置css加载器等
│ ├── webpack.base.conf.js --------------------------- webpack基本配置
│ ├── webpack.dev.conf.js ---------------------------- 用于开发的webpack设置
│ ├── webpack.prod.conf.js --------------------------- 用于打包的webpack设置
├── config ---------------------------------- 配置目录,包括端口号等。我们初学可以使用默认的。
│ ├── dev.env.js -------------------------- 开发环境变量
│ ├── index.js ---------------------------- 项目配置文件
│ ├── prod.env.js ------------------------- 生产环境变量
│ ├── test.env.js ------------------------- 测试环境变量
├── node_modules ---------------------------- npm 加载的项目依赖模块
├── src ------------------------------------- 我们要开发的目录,基本上要做的事情都在这个目录里。
│ ├── assets ------------------------------ 静态文件,放置一些图片,如logo等
│ ├── components -------------------------- 组件目录,存放组件文件,可以不用。
│ ├── main.js ----------------------------- 主js
│ ├── App.vue ----------------------------- 项目入口组件,我们也可以直接将组件写这里,而不使用 components 目录。
│ ├── router ------------------------------ 路由
├── static ---------------------------- 静态资源目录,如图片、字体等。
├── .babelrc--------------------------------- babel配置文件
├── .editorconfig---------------------------- 编辑器配置
├── .gitignore------------------------------- 配置git可忽略的文件
├── index.html ------------------------------ 首页入口文件,你可以添加一些 meta 信息或统计代码啥的。
├── package.json ---------------------------- node配置文件,记载着一些命令和依赖还有简要的项目描述信息
├── .README.md------------------------------- 项目的说明文档,markdown 格式。想怎么写怎么写,不会写就参照github上star多的项目,看人家怎么写的
在vue-cli的项目中,其中src文件夹是必须要掌握的,因为基本上要做的事情都在这个目录里。
index.html如其他html一样,但一般只定义一个空的根节点,在main.js里面定义的实例将挂载在根节点下,内容都通过vue组件来填充,构建的文件将会被自动注入,也就是说我们编写的其它的内容都将在这个 div 中展示。整个项目只有这一个 html 文件,所以这是一个 单页面应用,当我们打开这个应用,表面上可以有很多页面,实际上它们都只不过在一个 div 中。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>vuedemo</title>
</head>
<body>
<!-- 定义的vue实例将挂载在#app节点下 -->
<div id="app"></div>
</body>
</html>
这个文件称为“根组件”,因为其它的组件又都包含在这个组件中。.vue 文件是一种自定义文件类型,在结构上类似 html,一个 .vue 文件即是一个 vue 组件。
一个vue页面通常由三部分组成:模板(template)、js(script)、样式(style)
<template><template>
<div id="app">
<img src="./assets/logo.png">
<router-view></router-view>
</div>
</template>
<!-- script -->
<script>
export default {
name: 'app'
}
</script>
<div id="app">
<img src="./assets/logo.png">
<router-view></router-view>
</div>
</template>
<!-- script -->
<script>
export default {
name: 'app'
}
</script>
<!-- 样式 -->
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
其中模板只能包含一个父节点,也就是说顶层的div只能有一个(例如上面代码,父节点为#app的div,其没有兄弟节点)。这里也有一句 <div id="app">,但跟 index.html 里的那个是没有关系的。这个id=app 只是跟下面的 css 对应。
<router-view></router-view>是子路由视图,后面的路由页面都显示在此处。打一个比喻吧,<router-view>类似于一个插槽,跳转某个路由时,该路由下的页面就插在这个插槽中渲染显示
<script>标签里的内容即该组件的脚本,也就是 js 代码,export default 是 ES6 的语法,意思是将这个组件整体导出,之后就可以使用 import 导入组件了。大括号里的内容是这个组件的相关属性。
vue通常用es6来写,用export default导出,其下面可以包含数据data,生命周期(mounted等),方法(methods)等,具体语法请看vue.js文档。
样式通过style标签包裹,默认是影响全局的,如需定义作用域只在该组件下起作用,需在标签上加scoped.
如要引入外部css文件,首先需给项目安装css-loader依赖包,打开cmd,进入项目目录,输入npm install css-loader,回车。
安装完成后,就可以在style标签下import所需的css文件,例如:
<style>
import './assets/css/public.css'
</style>
main.js主要是引入vue框架,根组件及路由设置,并且定义vue实例,下面的 components:{App}就是引入的根组件App.vue。后期还可以引入插件,当然首先得安装插件。
前面我们说 App.vue 里的<div id="app"> 和 index.html 里的<div id="app"> 没有关系,那么这两个文件是怎么建立联系的呢?让我们来看入口文件 main.js 的代码
/*引入vue框架*/
import Vue from 'vue'
/*引入根组件*/
import App from './App'
/*引入路由设置*/
import router from './router'
/*关闭生产模式下给出的提示*/
Vue.config.productionTip = false
/*定义实例*/
new Vue({
el: '#app',
router,
template: '<App/>',
components: { App }
})
最上面 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 的官方地址为 http://element-cn.eleme.io/#/zh-CN
根据官方文档的描述,在项目文件夹下,执行 npm i element-ui -S 即可
引入分为完整引入和按需引入两种模式,按需引入可以缩小项目的体积,这里我们选择完整引入。
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 开发的状态管理方案,我们可以把需要在各个组件中传递使用的变量、方法定义在这里。之前我一直没有使用它,所以在不同组件传值的问题上十分头疼,要写很多多余的代码来调用不同组件的值,所以推荐大家从一开始就去熟悉这种管理方式。
之后,在 src 目录下新建一个文件夹 store,并在该目录下新建 index.js 文件,在该文件中引入 vue 和 vuex,代码如下:
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 [email protected]
如果你的vue版本是 3.X ,将vuex升到 4.X.X 就能够解决
npm install --save [email protected]
npm install --save [email protected]
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
四、前端拦截器
4.1 配置登陆拦截器
拦截器顾名思义就是对请求的拦截,请求接口之前或之后的预处理工作。分别为请求拦截器和响应拦截器, 执行顺序: 请求拦截器 -> api请求 -> 响应拦截器。 拦截器的作用:a. 统计api从发起请求到返回数据需要的时间;b. 配置公共的请求头,加载弹窗等;c. 对响应状态码做拦截,比入后端返回400或500的状态码, 返回对应错误信息。
4.2 axios请求拦截器request
在vue项目中,我们通常使用axios与后台进行数据交互,axios是一款基于promise封装的库,可以运行在浏览器端和node环境中。请求拦截器request作用:在请求发送前统一执行某些操作,常用在请求头中处理token等
axios.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
})
4.3 axios响应拦截器response
返回对象response中有response.status:Http响应码;response.data:后端返回的Json对象,包括response.data.code业务逻辑响应码,response.data.message:后端返回的响应提示信息;
axios.interceptors.response.use(function (response) {
// 对响应数据做点什么
return response;
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});
}
4.4 封装请求
在项目中,我们并不会直接使用 axios,而是会对它进行一层封装。 通过export导出封装的请求,如定义一个postRequest方法接收url和params,然后axios对象。在axios里进行实际接口调用操作。
export const postRequest = (url, params) => {
return axios({
method: 'post',
url: `${base}${url}`,
data: params
})
}
4.5 代码实现src/utils/api.js
import axios from "axios";
import {Message} from "element-ui";
import router from "@/router";
// 请求拦截器
axios.interceptors.request.use(config => {
// 如果存在 token,请求携带这个 token( 登录的时候 把 token 存入了 sessionStorage )
if (window.sessionStorage.getItem("tokenStr")) {
// token 的key : Authorization ; value: tokenStr
config.headers['Authorization'] = window.sessionStorage.getItem('tokenStr')
}
return config;
},error => {
console.log(error)
})
// 响应拦截器 - 统一处理消息提示
axios.interceptors.response.use(success => {
// 业务逻辑错误
if (success.status && success.status === 200) { // 调到接口
// 后端:500 业务逻辑错误,401 未登录,403 无权访问;
if (success.data.code === 500 || success.data.code === 401 || success.data.code === 403) {
Message.error({message: success.data.message})
return
}
if (success.data.message) { // 输出后端 添加成功 之类的信息
Message.success({message: success.data.message})
}
}
return success.data
}, error => { // 没访问到后端接口
if (error.response.code === 504 || error.response.code === 404) {
Message.error({message: '服务器不存在'})
} else if (error.response.code === 403) {
Message.error({message: '权限不足,请联系管理员!'})
} else if (error.response.code === 401) {
Message.error({message: '您还未登录,请登录!'})
router.replace('/') // 路由替换
} else {
if (error.response.data.message) {
Message.error({message: error.response.data.message})
} else {
Message.error({message: '未知错误!'})
}
}
return
})
// 预备前置路径
let base = '';
// 传送 json 格式的 post 请求
export const postRequest = (url, params) => {
return axios({
method: 'post',
url: `${base}${url}`,
data: params
})
}
// 传送 json 格式的 get 请求
export const getRequest = (url, params) => {
return axios({
method: 'get',
url: `${base}${url}`,
data: params
})
}
// 传送 json 格式的 put 请求
export const putRequest = (url, params) => {
return axios({
method: 'put',
url: `${base}${url}`,
data: params
})
}
// 传送 json 格式的 delete 请求
export const deleteRequest = (url, params) => {
return axios({
method: 'delete',
url: `${base}${url}`,
data: params
})
}
拦截器顾名思义就是对请求的拦截,分别为请求拦截器和响应拦截器, 执行顺序: 请求拦截器 -> api请求 -> 响应拦截器。 拦截器的作用:a. 统计api从发起请求到返回数据需要的时间;b. 配置公共的请求头,加载弹窗等;c. 对响应状态码做拦截,比入后端返回400或500的状态码, 返回对应错误信息。
4.6 main.js全局引入封装请求
通过main.js全局引入然后通过插件的方式使用方法。在具体调用时使用this.putRequest(url,params)形式使用
import {postRequest} from "@/utils/api";
import {putRequest} from "@/utils/api";
import {getRequest} from "@/utils/api";
import {deleteRequest} from "@/utils/api";
Vue.prototype.postRequest = postRequest
Vue.prototype.putRequest = putRequest
Vue.prototype.getRequest = getRequest
Vue.prototype.deleteRequest = deleteRequest
五、登陆页面
5.1 样式设计
为了设计界面,我们需要关注的地方是 <template> 标签内的 html 和 <style> 标签内的 css。登录框我们一般会用 Form 来做,打开 Element 的组件文档(http://element-cn.eleme.io/#/zh-CN/component/),发现它为我们提供了丰富的 Form 组件,我们可以点击“显示代码”,复制我们需要的部分。
不过这里好像并没有特别符合我们应用场景的表单,或者说这些都是比较复杂的,我们只需要其中的一小部分。把页面再往下拉,可以看到关于这个组件的属性、事件、方法等的文档,根据这个文档,我们可以自己去构建需要的表单。
5.2 登陆页功能设计
5.3 Login.vue登录页
验证码通过后端返回图片。表单通过rules绑定规则,通过prop为元素添加属性,在rules里写规则。验证方式:this.$refs.loginForm.validate。
/login登陆返回报文
登陆成功后会返回一个token。此token会作为后面前后端数据交互的一个凭证。为了保证系统安全性后端会定期更新该token,超过token的失效之后用户需要重新登录。前端将获取来的token存入sessionStorage中作为之后调用接口的钥匙,此后通过Axios进行get或者post请求时都需要带上此token。
在请求拦截器里判断toke是否存在,之后每次请求都会校验token,如果存在则请求携带token,放入Authorization参数中;后端校验token。
前端登陆成功后 通过this.$router.replace('/home') 跳转到home首页。replace方法替换后点击浏览器回退按钮不会跳转到登陆页面。登陆失败后端返回失败原因。
在用户未登陆的情况下,如果用户不是以http://localhost:8080/#/访问登陆页,而是访问某个登陆后才能访问的路由,如http://localhost:8080/#/sys/basic。需要分情况讨论:1.用户可能输入首页地址或错误地址,登陆成功后让他跳到首页;2.否则成功跳转到他输入的地址。
this.$router.replace((path === '/' || path === undefined) ? '/home' : path)
<template>
<div>
<el-form
v-loading="loading"
element-loading-text="正在登录......"
element-loading-spinner="el-icon-loading"
element-loading-background="rgba(0, 0, 0, 0.8)"
ref="loginForm" :model="loginForm" :rules="rules" class="loginContainer">
<h3 class="loginTitle">系统登录</h3>
<el-form-item prop="username">
<el-input type="text" v-model="loginForm.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input type="password" v-model="loginForm.password" placeholder="请输入密码"></el-input>
</el-form-item>
<el-form-item prop="code">
<el-input type="text" v-model="loginForm.code" placeholder="点击图片更换验证码"
style="width: 250px;margin-right: 5px;"></el-input>
<img :src="captchaUrl" @click="updateCaptcha">
</el-form-item>
<el-button type="primary" style="width: 100%" @click="submitLogin">登录</el-button>
</el-form>
</div>
</template>
<script>
export default {
name: 'Login',
components: {},
props: [],
data() {
return {
// 验证码
captchaUrl:'/captcha?time=' + new Date(),//获取响应码后端接口
loginForm: {
username: 'admin',
password: '123',
code: '',
},
loading: false, // 加载中
//校验规则,与表单绑定
rules: {
username: [{required: true, message: '请输入用户名', trigger: 'blur'}],
password: [{required: true, message: '请输入密码', trigger: 'blur'}],
code: [{required: true, message: '请输入验证码', trigger: 'blur'}]
}
}
},
mounted(){
},
methods: {
// 点击刷新验证码
updateCaptcha() {
this.captchaUrl="/captcha?time="+new Date();
},
submitLogin() {
// 登录
this.$refs.loginForm.validate((valid) => {
if (valid) {
this.loading = true;//准备调登录接口时,出现正在加载
//第一个参数请求后端的地址,第二个参数,传给后端的数据
this.postRequest('/login', this.loginForm).then(resp => {
this.loading = false;//登录成功后关闭
if (resp) {
// 存储用户 token 到 sessionStorage
const tokenStr = resp.obj.tokenHead + resp.obj.token;
window.sessionStorage.setItem('tokenStr', tokenStr);
// 跳转到首页
// this.$router.push('/home') // 路由跳转,可以回退到上一页
this.$router.replace('/home') // 路径替换,无法回退到上一页
// 页面跳转
// 拿到用户要跳转的路径
let path = this.$route.query.redirect;
// 用户可能输入首页地址或错误地址,让他跳到首页,否则跳转到他输入的地址
this.$router.replace((path === '/' || path === undefined) ? '/home' : path)
}
})
} else {
this.$message.error('请输入所有字段!');
return false;
}
})
}
}
}
</script>
<style>
.loginContainer {
border-radius: 15px;
background-clip: padding-box;
/*属性规定背景的绘制区域 背景被裁剪到内边距框。 margin: 180 px auto;*/
margin: 180px auto;
width: 350px;
padding: 15px 35px;
background: #fff;
border: 1px solid #eaeaea;
box-shadow: 0 0 25px #cac6c6;
/* X轴偏移量 Y轴偏移量 [阴影模糊半径] [阴影扩展] [阴影颜色] [投影方式]; */
}
.loginTitle {
margin: 0 auto 40px auto;
text-align: center;
}
.loginRemember {
text-align: left;
margin: 0 0 15px 0;
}
/*验证码*/
.el-form-item__content {
display: flex;
align-items: center;
}
</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
import Vue from 'vue'
import Router from 'vue-router'
import Login from "@/views/Login";
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'Login',
component: Login,
hidden: true // 不会被循环遍历出来
},
]
})
5.5 前端路由导航守卫
登录页面的开发似乎已经较为完善了,但其实还没有完,因为这个登录页面其实没有用,别人直接输入首页的网址,就可以绕过登录页面。为了让它发挥作用,我们还需要开发一个拦截器。使用钩子函数判断是否拦截函数及在某些时机会被调用的函数。这里我们使用 router.beforeEach(),意思是在访问每一个路由前调用。to 要去的路由; from 来自哪里的路由 ; next() 放行。
通过sessionStorage.getItem('user')获取用户的token,如果token不存在则需要登陆。
在判断是否为if (to.path == '/')登陆页,是的话放行,否则按用户指定的路由登陆;
main.js
// 使用 router.beforeEach 注册一个全局前置守卫
router.beforeEach((to, from, next) => {
// to 要去的路由; from 来自哪里的路由 ; next() 放行
// 用户登录成功时,把 token 存入 sessionStorage,如果携带 token,初始化菜单,放行
if (window.sessionStorage.getItem('tokenStr')) {
// 如果用户不存在
//待首页功能部分完善后补充
} else {
if (to.path === '/') {
next()
} else {
next('/?redirect=' + to.path)
}
}
})
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)
前端反向代理
修改proxyTable 请求地址经过node.js后代理到后端地址8081
proxyTable: {
'/': {
changeOrigin: true, //跨域
target: 'http://localhost:8081',
pathRewrite: {
// '^/api': ''
}
},
},
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
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
// 导入 Vuex
const store = new Vuex.Store({
state: {
routes: []
},
mutations: { // 与 state 同步执行;可以改变 state 对应的值的方法
// 初始化路由 菜单
initRoutes(state, data) {
state.routes = data
},
},
// 异步执行
actions: {
}
})
export default store;
在Main.js中引入store
import store from './store'
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
6.2 封装菜单请求工具
后端请求菜单接口返回信息
我们设计的菜单是根据用户信息加载的路由信息,即不同用户可能有不同的菜单权限。接口返回的菜单信息如下。通过children表示子菜单,子菜单中的parentId与父菜单的id相等时表示一个确定的父子菜单关系。如下的关系表示有一个层级菜单“员工资料/基本资料”。
如果store.state.routes有数据,初始化路由菜单。通过getRequest('/system/config/menu')方法从后端获取路由数据,按照层次关系拆分。
如何根据接口中的component字段找到对应的代码路径呢?
通过对接口对象中的component字段分类查找,例如component以Home开头,源代码在src/views/Home.vue中。
if (component.startsWith('Home')) {
require(['@/views/' + component + '.vue'], resolve);
}
initMenu方法将路由数据存于store中,如果store中有数据则无需初始化,否则,初始化。
什么时候调用?每一个页面都需要调用初始化菜单方法。放在路由拦截器里,每次访问路由都要执行一次。
menus.js
import {getRequest} from "@/utils/api";
// 菜单请求工具类
// router 路由; store Vuex
export const initMenu = (router, store) => {
// 如果有数据,初始化路由菜单
if (store.state.routes.length > 0) {
return;
}
getRequest('/system/config/menu').then(data => {
// 如果数据存在 格式化路由
if (data) {
// 格式化好路由
let fmtRoutes = formatRoutes(data)
// 添加到 router
router.addRoutes(fmtRoutes)
// 将数据存入 Vuex
store.commit('initRoutes',fmtRoutes)
// 连接 WebSocket
store.dispatch('connect')
}
})
}
export const formatRoutes = (routes) => {
let fmtRoutes = []
routes.forEach(router => {
let {
path,
component,
name,
iconCls,
children
} = router;
// 如果有 children 并且类型是数组
if (children && children instanceof Array) {
// 递归
children = formatRoutes(children)
}
// 单独对某一个路由格式化 component
let fmRouter = {
path: path,
name: name,
iconCls: iconCls,
children: children,
component(resolve) {
// 判断组件以什么开头,到对应的目录去找
if (component.startsWith('Home')) {
require(['@/views/' + component + '.vue'], resolve);
}else if (component.startsWith('Emp')) {
require(['@/views/emp/' + component + '.vue'], resolve);
}else if (component.startsWith('Per')) {
require(['@/views/per/' + component + '.vue'], resolve);
}else if (component.startsWith('Sal')) {
require(['@/views/sal/' + component + '.vue'], resolve);
}else if (component.startsWith('Sta')) {
require(['@/views/sta/' + component + '.vue'], resolve);
}else if (component.startsWith('Sys')) {
require(['@/views/sys/' + component + '.vue'], resolve);
}
}
}
fmtRoutes.push(fmRouter)
})
return fmtRoutes
}
更新main.js
将当前用户信息保存在sessionStorage的user中,每次路由切换时获取用户的登陆信息。
// 使用 router.beforeEach 注册一个全局前置守卫
router.beforeEach((to, from, next) => {
// to 要去的路由; from 来自哪里的路由 ; next() 放行
// 用户登录成功时,把 token 存入 sessionStorage,如果携带 token,初始化菜单,放行
if (window.sessionStorage.getItem('tokenStr')) {
initMenu(router, store)
// 如果用户不存在
if (!window.sessionStorage.getItem('user')
) {
// 判断用户信息是否存在
return getRequest('/admin/info').then(resp => {
if (resp) {
// 存入用户信息,转字符串,存入 sessionStorage
window.sessionStorage.setItem('user', JSON.stringify(resp))
// 同步用户信息 编辑用户
store.commit('INIT_ADMIN',resp)
next();
}
})
}
next();
} else {
if (to.path === '/') {
next()
} else {
next('/?redirect=' + to.path)
}
}
})
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代码
<template>
<div>
<el-container>
<el-header class="homeHeader">
<div class="title">云办公</div>
<!-- 1-1 添加在线聊天入口 -->
<div>
<el-button type="text" icon="el-icon-bell" size="normal"
style="margin-right: 8px;color: black;" @click="goChar"></el-button>
<el-dropdown class="userInfo" @command="commandHandler">
<span class="el-dropdown-link">
{
{ user.name }}<i><img :src="user.userFace"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="userinfo">个人中心</el-dropdown-item>
<el-dropdown-item command="setting">设置</el-dropdown-item>
<el-dropdown-item command="logout">注销登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</el-header>
<el-container>
<el-aside width="200px">
<!-- 1、添加 router -->
<el-menu router unique-opened>
<!-- 2、循环整个路由组件,不展示 hidden: true 的路由组件 -->
<el-submenu :index="index +''" v-for="(item,index) in routes"
:key="index" v-if="!item.hidden">
<template slot="title"><i :class="item.iconCls" style="color: black;margin-right: 5px"></i>
<span>{
{ item.name }}</span>
</template>
<!-- 3、循环遍历子路由 -->
<el-menu-item :index="children.path"
v-for="(children,index) in item.children" :key="index">{
{ children.name }}
</el-menu-item>
</el-submenu>
</el-menu>
</el-aside>
<el-main>
<!-- 面包屑导航区域 -->
<el-breadcrumb separator-class="el-icon-arrow-right"
v-if="this.$router.currentRoute.path!=='/home'">
<el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>{
{ this.$router.currentRoute.name }}</el-breadcrumb-item>
</el-breadcrumb>
<div class="homeWelcome" v-if="this.$router.currentRoute.path==='/home'">
欢迎来到云办公系统!
</div>
<!-- 路由点位符 -->
<router-view class="homeRouterView"/>
</el-main>
</el-container>
</el-container>
</div>
</template>
<script>
export default {
name: 'Home',
data() {
return {
// 获取用户信息,将字符串转对象
// user: JSON.parse(window.sessionStorage.getItem('user'))
}
},
computed: {
// 从 vuex 获取 routes
routes() {
return this.$store.state.routes
},
user() {
return this.$store.state.currentAdmin
}
},
methods: {
// 1-2 进入在线聊天页面
goChar() {
this.$router.push('/chat')
},
// 注销登录
commandHandler(command) {
if (command === 'logout') {
// 弹框提示用户是否要删除
this.$confirm('此操作将注销登录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 注销登录
this.postRequest('/logout')
// 清空用户信息
window.sessionStorage.removeItem('tokenStr')
window.sessionStorage.removeItem('user')
// 路由替换到登录页面
// this.$router.replace('/')
// 清空菜单信息;在src/utils/menus.js 中初始化菜单信息
this.$store.commit('initRoutes', [])
this.$router.replace('/')
}).catch(() => {
this.$message({
type: 'info',
message: '已取消注销登录'
});
});
}
if (command === 'userinfo') {
this.$router.push('/userinfo')
}
}
}
}
</script>
<style scoped>
.homeHeader {
background: #3e9ef5;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 15px;
box-sizing: border-box;
}
.homeHeader .title {
font-size: 30px;
/*font-family: 微软雅黑;*/
font-family: 华文楷体;
color: white;
}
.homeHeader .userInfo {
cursor: pointer;
}
.el-dropdown-link img {
width: 48px;
height: 48px;
border-radius: 50%;
margin-left: 8px;
}
.homeWelcome {
text-align: center;
font-size: 30px;
font-family: 华文楷体;
color: #409ef4;
padding-top: 50px;
}
.homeRouterView {
margin-top: 10px;
}
</style>
<!-- 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的
import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from "@/views/Login";
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Login',
component: Login,
hidden: true // 不会被循环遍历出来
}
]
const router = new VueRouter({
routes
})
export default router
6.6 index.html消除边距
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>yeb-front</title>
</head>
<body style="margin:0px;padding:0px">
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
七、基础信息设置
7.1 样式设计
系统管理/基础信息设置设计如下几个模块:部门管理、职位管理、职称管理、奖惩规则、权限组
Tabs 标签页
使用element的Tabs标签页完成不同业务功能的切换;分隔内容上有关联但属于不同类别的数据集合。
abs 组件提供了选项卡功能,默认选中第一个标签页,你也可以通过 value 属性来指定当前选中的标签页。
<template>
<el-tabs v-model="activeName" @tab-click="handleClick">
<el-tab-pane label="用户管理" name="first">用户管理</el-tab-pane>
<el-tab-pane label="配置管理" name="second">配置管理</el-tab-pane>
<el-tab-pane label="角色管理" name="third">角色管理</el-tab-pane>
<el-tab-pane label="定时任务补偿" name="fourth">定时任务补偿</el-tab-pane>
</el-tabs>
</template>
<script>
export default {
data() {
return {
activeName: 'second'
};
},
methods: {
handleClick(tab, event) {
console.log(tab, event);
}
}
};
</script>
7.2 组件化开发
在我们开发的过程中会遇到很多可以重复使用的代码块,而Vue则提供了这样的封装方式也就是Vue.component。利用组件化开发,将部门管理、职位管理、职称管理、奖惩规则、权限组等使用组件方式引入。组件也是.vue文件。组件导入方式 import 组件名 from “组件路径”,示例:
import DepMana from "@/components/sys/basic/DepMana";导入后并不能直接使用,需要在components对象中注册组件。之后是组件的应用:<DepMana/>或<DepMana></DepMana/>
7.3 SysBasic.vue
<template>
<div>
<el-tabs v-model="activeName" type="card">
<el-tab-pane label="部门管理" name="DepMana"><DepMana/></el-tab-pane>
<el-tab-pane label="职位管理" name="PosMana"><PosMana/></el-tab-pane>
<el-tab-pane label="职称管理" name="JobLevelMana"><JobLevelMana/></el-tab-pane>
<el-tab-pane label="奖惩规则" name="EcMana"><EcMana/></el-tab-pane>
<el-tab-pane label="权限组" name="PositionMana"><PositionMana/></el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import DepMana from "@/components/sys/basic/DepMana"; // 部门管理
import EcMana from "@/components/sys/basic/EcMana"; // 奖惩规则
import JobLevelMana from "@/components/sys/basic/JobLevelMana"; // 职称管理
import PositionMana from "@/components/sys/basic/PositionMana"; // 权限组
import PosMana from "@/components/sys/basic/PosMana"; // 职位管理
export default {
name: "SysBasic",
components:{
JobLevelMana,
DepMana,
EcMana,
PositionMana,
PosMana
},
data() {
return {
activeName: 'DepMana' // 激活项
}
},
methods: {}
}
</script>
<style scoped>
</style>
7.4 部门管理DepMana.vue组件
样式设计:flex布局,space-between:两端对齐,与父元素对齐宽度。
搜索框过滤节点的方法,通过filterNode方法传入两个参数,value-搜索输入的值,data树形标签绑定的数据。value为空时展示所有标签值,否则,判断输入的value能否在data.name(展示在树形标签上的值)找到,这里用了js查找字符串方法indexOf,返回查找元素的下标,能找到返回下标(>=0)
expand-on-click-node仅当鼠标点击展开按钮时展开,方便添加和删除功能的使用。
点击按钮时获取当前节点的数据即要添加子部门的“上级部门”id(parentId)
<template>
<div style="width: 500px">
<!-- 1 -->
<el-input
placeholder="请输入部门名称进行搜索..."
prefix-icon="el-icon-search"
v-model="filterText">
</el-input>
<!-- 9、:expand-on-click-node="false" 点击小三角箭头才会展开
:default-expand-all="false" 设置默认不展开所有节点 -->
<el-tree
:data="deps"
:props="defaultProps"
default-expand-all
:filter-node-method="filterNode"
:expand-on-click-node="false"
ref="tree">
<!-- 7、label: 'name' -->
<!-- 8、style="display: flex;justify-content: space-between;width: 100% 父容器宽度" 让添加和删除按键居右 -->
<span class="custom-tree-node" slot-scope="{ node, data }"
style="display: flex;justify-content: space-between;width: 100%">
<span>{
{ data.name }}</span>
<span>
<el-button
plain
type="primary"
size="mini"
class="depBtn"
@click="() => showAddDep(data)">
添加部门
</el-button>
<!-- 10、showAddDep(data) deleteDep(data) data 后端传过来的完整的 json 对象 -->
<el-button
plain
type="danger"
size="mini"
class="depBtn"
@click="() => deleteDep(data)">
删除部门
</el-button>
</span>
</span>
</el-tree>
<!-- 13、对话弹框 -->
<el-dialog
title="添加部门"
:visible.sync="dialogVisible"
width="30%">
<!-- 16 -->
<div>
<table>
<tr>
<td>
<el-tag>上级部门</el-tag>
</td>
<td>{
{ pname }}</td>
</tr>
<tr>
<td>
<el-tag>部门名称</el-tag>
</td>
<td>
<el-input v-model="dep.name" placeholder="请输入部门名称..." size="small"></el-input>
</td>
</tr>
</table>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<!-- 18、确定添加按钮绑定事件 @click="doAddDep" -->
<el-button type="primary" @click="doAddDep">确 定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
export default {
name: "DepMana",
data() {
return { // 2
filterText: '',
deps: [], // 所有部门整个数组
defaultProps: { // 2 关联子部门
children: 'children',
label: 'name'
},
dialogVisible: false, // 14
dep: { // 15、添加部门数据象
name: '',
parentId: -1,
isParent: ''
},
pname: '' // 15、上级部门名称
}
},
watch: {
// 4、观察者事件,监控输入框的值(框架方法)
filterText(val) {
this.$refs.tree.filter(val);
}
},
mounted() {
this.initDeps() // 6、调用获取所有部门方法
},
methods: {
// 删除部门调用的方法
removeDepFromDeps(p, deps, id) {
for (let i = 0; i < deps.length; i++) {
let d = deps[i]
if (d.id === id) {
deps.splice(i, 1)
if (deps.length === 0) {
p.isParent = false
}
return;
} else {
this.removeDepFromDeps(d, d.children, id)
}
}
},
// 12、删除部门
deleteDep(data) {
// console.log(data)
if (data.isParent) {
this.$message.error('父部门删除失败!')
} else {
this.$confirm('此操作将永久删除该[' + data.name + ']部门, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.deleteRequest('/system/basic/department/' + data.id).then(resp => {
if (resp) {
this.removeDepFromDeps(null, this.deps, data.id)
}
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
}
},
// 20、添加完部门 初始化 清空数据
initDep() {
this.dep = {
name: '',
parentId: -1
}
this.pname = ''
},
// 22、 递归查询所有部门信息,deps 查询到的整个数组,dep 添加的部门
addDep2Deps(deps, dep) {
for (let i = 0; i < deps.length; i++) {
let d = deps[i] // 父部门
if (d.id === dep.parentId) {
d.children = d.children.concat(dep) // 把 dep 加为 d 的子部门
if (d.children.length > 0) {
d.isParent = true
}
return;
} else {
this.addDep2Deps(d.children, dep) // 递归调用此方法 以查询结果为条件 继续查询子部门
}
}
},
// 19、确认添加部门
doAddDep() {
this.postRequest('/system/basic/department/', this.dep).then(resp => {
if (resp) {
// console.log(resp)
this.dialogVisible = false // 关闭对话框
this.addDep2Deps(this.deps, resp.data) // 23、【无效】手动插入部门 显示添加后的数据
this.initDep() // 21、调用初始化方法 清空数据
}
})
},
// 11、17、添加部门弹框
showAddDep(data) {
// console.log(data)
this.dep.parentId = data.id
this.pname = data.name
this.dialogVisible = true
},
// 5、获取所有部门
initDeps() {
this.getRequest('/system/basic/department/').then(resp => {
if (resp) {
this.deps = resp
}
})
},
// 3、事件(框架方法)
filterNode(value, data) { // data 整行数据
if (!value) return true; // true 节点可以展示,false 节点隐藏
return data.name.indexOf(value) !== -1; // label: 'name'
}
}
}
</script>
<style scoped>
/* 8 */
.depBtn {
padding: 2px;
}
</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的数据修改。
<template>
<div>
<div>
<el-input
size="small"
class="addPosInput"
placeholder="请选择日期"
suffix-icon="el-icon-plus"
@keydown.enter.native="addPosition"
v-model="pos.name">
</el-input>
<el-button size="small" icon="el-icon-plus" type="primary" @click="addPosition">添加</el-button>
</div>
<div class="posManaMain">
<el-table
border
stripe
size="small"
:data="positions"
style="width: 70%"
@selection-change="handleSelectionChange">
<el-table-column
type="selection"
width="55">
</el-table-column>
<el-table-column
prop="id"
label="编号"
width="55">
</el-table-column>
<el-table-column
prop="name"
label="职位"
width="120">
</el-table-column>
<el-table-column
prop="createDate"
label="创建时间"
width="200">
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button
size="mini"
@click="showEditView(scope.$index, scope.row)">编辑
</el-button>
<el-button
size="mini"
type="danger"
@click="handleDelete(scope.$index, scope.row)">删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- :disabled 不禁用条件为勾选中,没勾选中为禁用。 -->
<el-button size="small" style="margin-top: 8px" type="danger"
:disabled="this.multipleSelection.length===0" @click="deleteMany">批量删除
</el-button>
<el-dialog
title="提示"
:visible.sync="dialogVisible"
width="30%">
<div>
<el-tag>职位名称</el-tag>
<el-input v-model="updatePos.name" size="small" class="updatePosInput"></el-input>
</div>
<span slot="footer" class="dialog-footer">
<el-button size="small" @click="dialogVisible = false">取 消</el-button>
<el-button size="small" type="primary" @click="doUpdate">确 定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
export default {
name: "PosMana",
data() {
return {
pos: { // 查询添加职位数据
name: ''
},
positions: [],
dialogVisible: false,
updatePos: { // 更新职位数据
name: ''
},
multipleSelection: [] // 批量删除勾选的对象
}
},
mounted() {
// 调用获取后端接口所有职位数据方法
this.initPositions()
},
methods: {
// 批量删除请求
deleteMany() {
this.$confirm('此操作将永久删除[' + this.multipleSelection.length + ']条职位, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
let ids = '?'
this.multipleSelection.forEach(item => {
ids += 'ids=' + item.id + '&'
})
this.deleteRequest('/system/basic/pos/' + ids).then(resp => {
if (resp) {
this.initPositions()
}
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
},
// 批量删除(取值)
handleSelectionChange(val) {
this.multipleSelection = val
// console.log(val)
},
// 编辑职位
doUpdate() {
this.putRequest('/system/basic/pos/', this.updatePos).then(resp => {
if (resp) {
this.initPositions() // 刷新数据列表
this.dialogVisible = false // 关闭对话框
}
})
},
// 编辑职位对话框
showEditView(index, data) {
Object.assign(this.updatePos, data) // 回显数据,数据拷贝
// this.updatePos = data // 回显数据 有bug
this.updatePos.createDate = ''
this.dialogVisible = true // 显示编辑框
},
// 添加职位
addPosition() {
if (this.pos.name) {
this.postRequest('/system/basic/pos/', this.pos).then(resp => {
if (resp) {
this.initPositions()
this.pos.name = ''
}
})
} else {
this.$message.error('职位名称不能为空!')
}
},
// 删除职位
handleDelete(index, data) {
this.$confirm('此操作将永久删除[' + data.name + ']职位, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.deleteRequest(' /system/basic/pos/' + data.id).then(resp => {
if (resp) {
this.initPositions()
}
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
},
// 获取后端接口所有职位数据
initPositions() {
this.getRequest('/system/basic/pos/').then(resp => {
if (resp) {
this.positions = resp
}
})
}
}
}
</script>
<style scoped>
/*添加职位输入框*/
.addPosInput {
width: 300px;
margin-right: 8px;
}
/*所有数据表格*/
.posManaMain {
margin-top: 10px;
}
/*编号职位输入框*/
.updatePosInput {
width: 200px;
margin-left: 8px;
}
</style>
7.6 职称管理JobLevelMana.vue组件
JobLevelMana.vue
<template>
<div>
<div>
<el-input size="small" v-model="jl.name" placeholder="添加职称名称..."
prefix-icon="el-icon-plus" style="width: 300px"></el-input>
<el-select size="small" v-model="jl.titleLevel" placeholder="职称等级" style="margin-left: 6px;margin-right: 6px">
<el-option
v-for="item in titleLevels"
:key="item"
:label="item"
:value="item">
</el-option>
</el-select>
<el-button type="primary" icon="el-icon-plus" size="small" @click="addJobLevel">添加</el-button>
</div>
<div style="margin-top: 10px;">
<el-table
:data="jls"
stripe
border
size="small"
style="width: 70%;"
@selection-change="handleSelectionChange">
<el-table-column
type="selection"
width="55">
</el-table-column>
<el-table-column
prop="id"
label="编号"
width="55">
</el-table-column>
<el-table-column
prop="name"
label="职称名称"
width="150">
</el-table-column>
<el-table-column
prop="titleLevel"
label="职称等级"
width="150">
</el-table-column>
<el-table-column
prop="createDate"
label="创建日期"
width="150">
</el-table-column>
<el-table-column
prop="enabled"
label="是否启用"
width="100">
<template slot-scope="scope">
<el-tag type="success" v-if="scope.row.enabled">已启用</el-tag>
<el-tag type="danger" v-else>未启用</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="350">
<template slot-scope="scope">
<el-button
size="small"
@click="showEditView(scope.row)">编辑
</el-button>
<el-button
size="small"
type="danger"
@click="deleteHandle(scope.row)">删除
</el-button>
</template>
</el-table-column>
</el-table>
<el-button size="small" style="margin-top: 8px" type="danger"
:disabled="this.multipleSelection.length===0" @click="deleteMany">批量删除
</el-button>
</div>
<!-- 编辑弹框 -->
<el-dialog
title="编辑职称"
:visible.sync="dialogVisible"
width="30%">
<table>
<tr>
<td>
<el-tag>职称名称</el-tag>
</td>
<td>
<el-input v-model="updateJl.name" size="small" style="margin-left: 6px"></el-input>
</td>
</tr>
<tr>
<td>
<el-tag>职称等级</el-tag>
</td>
<td>
<el-select size="small" v-model="updateJl.titleLevel" placeholder="职称等级"
style="margin-left: 6px;margin-right: 6px">
<el-option
v-for="item in titleLevels"
:key="item"
:label="item"
:value="item">
</el-option>
</el-select>
</td>
</tr>
<tr>
<td>
<el-tag>是否启用</el-tag>
</td>
<td>
<el-switch
style="margin-left: 6px"
v-model="updateJl.enabled"
active-color="#13ce66"
inactive-color="#ff4949"
active-text="启用"
inactive-text="未启用">
</el-switch>
</td>
</tr>
</table>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doUpdate">确 定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
export default {
name: "JobLevelMana",
data() {
return {
// 查询 添加 数据对象
jl: {
name: '',
titleLevel: ''
},
// 更新 数据对象
updateJl: {
name: '',
titleLevel: '',
enabled: false
},
titleLevels: [
'正高级',
'副高级',
'中级',
'初级',
'员级'
],
jls: [], // 删除单条
dialogVisible: false,
multipleSelection: [] // 批量删除勾选中的值
}
},
mounted() {
this.initJls()
},
methods: {
// 执行批量删除
deleteMany(){
this.$confirm('此操作将永久删除[' + this.multipleSelection.length + ']条职称, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
let ids = '?'
this.multipleSelection.forEach(item => {
ids += 'ids=' + item.id + '&'
})
this.deleteRequest('/system/basic/joblevel/' + ids).then(resp => {
if (resp) {
this.initJls()
}
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
},
// 批量删除勾选中的值
handleSelectionChange(val){
this.multipleSelection = val
},
// 更新职称信息
doUpdate() {
this.putRequest('/system/basic/joblevel/', this.updateJl).then(resp => {
if (resp) {
this.initJls()
this.dialogVisible = false
}
})
},
// 显示编辑弹框
showEditView(data) {
Object.assign(this.updateJl, data) // 复制数据,注意这里是 , 号隔开
this.updateJl.createDate = '' // 更新日期由后端处理,这里不用传
this.dialogVisible = true // 显示编辑弹框
},
// 删除职称
deleteHandle(data) {
this.$confirm('此操作将永久删除[' + data.name + ']职称, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.deleteRequest(' /system/basic/joblevel/' + data.id).then(resp => {
if (resp) {
this.initJls()
}
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
},
// 添加职称
addJobLevel() {
if (this.jl.name && this.jl.titleLevel) {
this.postRequest('/system/basic/joblevel/', this.jl).then(resp => {
if (resp) {
this.initJls()
}
})
} else {
this.$message.error('字段不能为空!')
}
},
// 获取职称列表数据
initJls() {
this.getRequest('/system/basic/joblevel/').then(resp => {
if (resp) {
this.jls = resp
this.jl.name = ''
this.jl.titleLevel = ''
}
})
}
}
}
</script>
<style scoped>
</style>
7.7 权限组PositionMana.vue组件
样式设计:由外到内依次使用了el-collapse折叠面板-》el-card卡片-》el-tree树形控件
<template slot="prepend">ROLE_</template>
使用element折叠面板展示角色,使用的是折叠面板的手风琴模式accordion,每次只能展开一个面板。
折叠面板每个角色的菜单访问权限,采用树形控件设置角色权限。el-tree添加show-checkbox属性展示可选框。
默认选中的角色菜单列表根据角色rid获取后端接口返回的菜单selectedMenus。通过:default-checked-keys方式绑定。
使用getCheckedKeys获取选中节点key组成的数组。let selectedKeys = tree.getCheckedKeys(true) // 获取选中的节点数组,true 仅返回被选中的叶子节点的 keys,如三级分类
<template>
<div>
<div class="positionManaTool">
<el-input v-model="role.name" placeholder="请输入角色英文名" size="small">
<template slot="prepend">ROLE_</template>
</el-input>
<el-input v-model="role.nameZh" placeholder="请输入角色中文名" size="small" @keydown.enter.native="doAddRole"></el-input>
<el-button type="primary" icon="el-icon-plus" size="mini" @click="doAddRole">添加角色</el-button>
</div>
<!-- 手风琴 -->
<div class="positionManaMain">
<el-collapse v-model="activeName" accordion @change="change">
<el-collapse-item :title="r.nameZh" :name="r.id" v-for="(r,index) in roles" :key="index">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>可访问资源</span>
<el-button type="text" icon="el-icon-delete" style="float: right;padding: 3px 0;color: #f41f0a" @click="doDeleteRole(r)">
</el-button>
</div>
<div>
<el-tree ref="treeRef" show-checkbox :data="allMenus" :props="defaultProps"
:default-checked-keys="selectedMenus"
node-key="id" :key="index"></el-tree>
<div style="display: flex;justify-content: flex-end">
<el-button size="mini" @click="cancelUpdate">取消修改</el-button>
<el-button size="mini" type="primary" @click="doUpdate(r.id,index)">确认修改</el-button>
</div>
</div>
</el-card>
</el-collapse-item>
</el-collapse>
</div>
</div>
</template>
<script>
export default {
name: "PositionMana",
data() {
return {
role: {
name: '',
nameZh: ''
},
roles: [],
allMenus: [],
defaultProps: { // 树形控件
children: 'children',
label: 'name' // 绑定数据 :name="r.id"
},
selectedMenus: [],
activeName: -1 // 折叠面板 默认关闭
}
},
mounted() {
this.initRoles()
this.initAllMenus()
},
methods: {
// 删除角色
doDeleteRole(role){
this.$confirm('此操作将永久删除[' + role.nameZh + ']角色, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.deleteRequest('/system/basic/permission/role/' + role.id).then(resp => {
if (resp) {
this.initRoles()
}
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
},
// 添加角色
doAddRole(){
if (this.role.name && this.role.nameZh) {
this.postRequest('/system/basic/permission/role',this.role).then(resp=>{
if (resp) {
this.initRoles()
this.role.name = ''
this.role.nameZh = ''
}
})
}else {
this.$message.error('所有字段不能为空!')
}
},
// 取消修改
cancelUpdate() {
this.activeName = -1 // 关闭折叠面板
},
// 确认修改
doUpdate(rid, index) {
let tree = this.$refs.treeRef[index] // 获取引用对象和索引
let selectedKeys = tree.getCheckedKeys(true) // 获取选中的节点数组,true 仅返回被选中的叶子节点的 keys,如三级分类
// console.log(selectedKeys)
let url = '/system/basic/permission/?rid=' + rid
selectedKeys.forEach(key => {
// 循环遍历出数组 id ,拼接在一起
url += '&mids=' + key
})
this.putRequest(url).then(resp => {
if (resp) {
this.activeName = -1 // 关闭折叠面板
}
})
},
// 手风琴点击事件
change(rid) {
if (rid) {
this.initAllMenus() // 调用获取所有菜单
this.initSelectedMenus(rid) // 调用获取所有选中的菜单
// alert(rid) // :name="r.id" label: 'name'
}
},
// 获取所有选中的菜单
initSelectedMenus(rid) { // :name="r.id" change(rid)
this.getRequest('/system/basic/permission/mid/' + rid).then(resp => {
if (resp) {
this.selectedMenus = resp
}
})
},
// 获取所有菜单
initAllMenus() {
this.getRequest('/system/basic/permission/menus').then(resp => {
if (resp) {
this.allMenus = resp
}
})
},
// 获取所有角色
initRoles() {
this.getRequest('/system/basic/permission/').then(resp => {
if (resp) {
this.roles = resp
}
})
}
}
}
</script>
<style scoped>
.positionManaTool {
display: flex;
justify-content: flex-start;
}
.positionManaTool .el-input {
width: 300px;
margin-right: 6px;
}
.positionManaMain {
margin-top: 10px;
width: 700px;
}
</style>
八、操作员管理
可以通过搜索操作员的名字,来单独显示操作员的信息。展示所有操作员的时候,不会把自己当前登录的操作员显示出来。
操作员涉及了权限:操作员拥有哪些角色,在根据角色再去拥有哪些菜单的权限。
SysAdmin.vue
<template>
<div>
<!-- 1、 -->
<div style="display: flex;justify-content: center;margin-top: 10px;">
<!-- 9、v-model="keywords" \ @click="doSearch">搜索 -->
<el-input v-model="keywords" placeholder="通过用户名搜索用户..." prefix-icon="el-icon-search"
style="width: 400px;margin-right: 10px;"></el-input>
<el-button type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
</div>
<!-- 2、6、 -->
<div class="admin-container">
<el-card class="admin-card" v-for="(admin,index) in admins" :key="index">
<div slot="header" class="clearfix">
<div class="userInfoTab">
<div style="margin-top:5px;margin-right: 8px;">{
{ admin.name}}</div>
<div><img :src="admin.userFace" :alt="admin.name" :title="admin.name" class="userFace-img"></div>
</div>
<!-- 12、 @click="deleteAdmin(admin)" -->
<el-button style="color:red;" type="text" icon="el-icon-delete"
@click="deleteAdmin(admin)"></el-button>
</div>
<div class="userinfo-container">
<div>用户名:{
{ admin.name }}</div>
<div>手机号码:{
{ admin.phone }}</div>
<div>电话号码:{
{ admin.telephone }}</div>
<div>地址:{
{ admin.address }}</div>
<div>用户状态:
<!-- 14、更新操作员 @change="enabledChange(admin)" -->
<el-switch
v-model="admin.enabled"
active-color="#13ce66"
inactive-color="#ff4949"
@change="enabledChange(admin)"
active-text="启用"
inactive-text="禁用">
</el-switch>
</div>
<div>
用户角色:
<el-tag style="margin-right: 4px;" type="success" v-for="(role,index) in admin.roles" :key="index">
{
{ role.nameZh }}
</el-tag>
<!-- 16、更新操作员角色 弹出框、选择器、 -->
<!-- 20、@show="showPop(admin)" -->
<!-- 24、@hide="hidePop(admin)" hide 隐藏时触发-->
<el-popover
placement="right"
title="角色列表"
width="200"
@show="showPop(admin)"
@hide="hidePop(admin)"
trigger="click">
<!-- 17、更新操作员角色 下拉框 -->
<!-- 22、v-model="selectedRoles" 存的是1个角色id,multiple 多选,显示已有角色 -->
<el-select v-model="selectedRoles" multiple placeholder="请选择">
<el-option
v-for="(r,index) in allRoles"
:key="index"
:label="r.nameZh"
:value="r.id">
</el-option>
</el-select>
<!-- 3个点按钮 ... -->
<el-button slot="reference" type="text" icon="el-icon-more"></el-button>
</el-popover>
</div>
<div>备注:{
{ admin.remark }}</div>
</div>
</el-card>
</div>
</div>
</template>
<script>
export default {
name: "SysAdmin",
data() {
return {
admins: [], // 3
keywords: '', // 8、搜索关键字
allRoles: [], // 18、更新操作员角色
selectedRoles: [] // 23
}
},
mounted() {
this.initAdmins() // 5
},
methods: {
// 25、更新操作员角色
hidePop(admin) {
let roles = []
Object.assign(roles, admin.roles) // 拷贝对象
let flag = false
// 如果选中的角色 id 的长度和原来的不一样
if (roles.length != this.selectedRoles.length) { // 用户对应角色id
flag = true
} else {
// 角色 id 长度和原来的一样,但可能角色不一样
// 先循环 admin.roles
for (let i = 0; i < roles.length; i++) {
let role = roles[i] // 用户对应的角色对象
for (let j = 0; j < this.selectedRoles.length; j++) {
let sr = this.selectedRoles[j] // 拿到用户对应的角色对象的id
if (role.id == sr) { // 角色一样
roles.splice(i, 1) // 删除
i--
break
}
}
}
if (roles.length != 0) {
flag = true
}
}
if (flag) {
// 拼接 url(参数为 adminId、角色 rids )
let url = '/system/admin/role?adminId=' + admin.id;
this.selectedRoles.forEach(sr => {
url += '&rids=' + sr
});
this.putRequest(url).then(resp => {
if (resp) {
this.initAdmins()
}
});
}
},
// 21、下拉框获取所有用户角色
showPop(admin) {
this.initAllRoles()
let roles = admin.roles // 拿到整个数组
this.selectedRoles = []
roles.forEach(r => {
this.selectedRoles.push(r.id) // r.id 相同的角色放进数组
})
},
// 19、获取所有操作员
initAllRoles() {
this.getRequest(' /system/admin/roles').then(resp => {
if (resp) {
this.allRoles = resp
}
})
},
// 15、更新操作员
enabledChange(admin) {
this.putRequest('/system/admin/', admin).then(resp => {
if (resp) {
this.initAdmins()
}
})
},
// 13、删除操作员
deleteAdmin(admin) {
this.$confirm('此操作将永久删除该[' + admin.name + '], 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.deleteRequest('/system/admin/' + admin.id).then(resp => {
if (resp) {
this.initAdmins()
}
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
},
// 10 搜索
doSearch() {
this.initAdmins()
},
// 4、获取所有操作员;11、加参数关键字
initAdmins() {
this.getRequest('/system/admin/?keywords=' + this.keywords).then(resp => {
if (resp) {
this.admins = resp
}
})
}
}
}
</script>
<style >
/* 7 */
.admin-container {
margin-top: 10px;
display: flex;
justify-content: space-between;
flex-wrap: wrap; /* 自动换行 */
}
.admin-card {
width: 280px;
margin-bottom: 20px;
}
.userInfoTab{
/* background-color: black; */
display: flex;
justify-content: center;
}
/* 卡片顶部 */
.clearfix {
display: flex;
justify-content: space-between;
}
.userFace-img {
width: 36px;
height: 36px;
border-radius: 36px;
}
/* 头像居中 */
/* .img-container {
/* width: 100%;
display: flex;
justify-content: center;
} */
.userinfo-container {
font-size: 12px;
color: #3e9ef5;
}
</style>
九、员工资料
axios本身不提供下载功能,需要安装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进行格式转换。
let fileDownload = require('js-file-download') // 插件
let fileName = headers['content-disposition'].split(';')[1].split('filename=')[1]//文件名
let contentType = headers['content-type'] // 响应类型
fileName = decodeURIComponent(fileName) // 格式转换 防止乱码
fileDownload(resp.data, fileName, contentType) // 通过插件下载文件
EmpBasic.vue
<template>
<div>
<div>
<div style="display: flex;justify-content: space-between;">
<!-- 1、 -->
<!-- 20、搜索 v-model="empName" <el-input @keydown.enter.native="initEmps" 回车键调用初始化会员方法
21、@click="initEmps">搜索</el-button>
22、清空 clearable @clear="initEmps" -->
<!-- 28-8 :disabled="showAdvanceSearchVisible" -->
<div style="margin-top: 10px;">
<el-input style="width: 300px;margin-right: 10px;"
prefix-icon="el-icon-search"
v-model="empName"
placeholder="请输入员工名进行搜索..."
@keydown.enter.native="initEmps"
clearable
@clear="initEmps"
:disabled="showAdvanceSearchVisible"
></el-input>
<el-button type="primary" icon="el-icon-search" @click="initEmps"
:disabled="showAdvanceSearchVisible">搜索
</el-button>
<!-- 28-3 @click="showAdvanceSearchVisible = !showAdvanceSearchVisible" -->
<!-- 28-5 判断图标样式 :class="showAdvanceSearchVisible?'fa fa-angle-double-up':'fa fa-angle-double-down'"-->
<el-button type="primary" @click="showAdvanceSearchVisible = !showAdvanceSearchVisible">
<i :class="showAdvanceSearchVisible?'fa fa-angle-double-up':'fa fa-angle-double-down'"
aria-hidden="true"></i>高级搜索
</el-button>
</div>
<div>
<!-- 27-1、3 导入数据 上传组件 用自己的按钮 -->
<!-- 27-5 on-success 文件上传成功时的钩子; on-error 文件上传失败时的钩子; -->
<!-- 27-8 导入的时候禁用导入按钮 :disabled="importDataDisabled" -->
<!-- 27-11 :headers="headers" 设置上传的请求头部 -->
<el-upload style="display: inline-flex;margin-right: 8px;" :show-file-list="false"
:headers="headers"
:before-upload="beforeUpload"
:on-success="onSuccess"
:on-error="onError"
:disabled="importDataDisabled"
action="/employee/basic/import"
>
<el-button type="success" :icon="importDataBtnIcon" :disabled="importDataDisabled">{
{
importDataBtnText
}}
</el-button>
</el-upload>
<!-- 26-1、导出数据 @click="exportData" -->
<el-button type="success" @click="exportData"><i class="el-icon-download" aria-hidden="true"></i> 导出数据
</el-button>
<!-- 23-3、 @click="showAddEmpView" -->
<el-button type="primary" icon="el-icon-plus" @click="showAddEmpView">添加员工</el-button>
</div>
</div>
<!-- 28-1 高级搜索条件框 -->
<!-- 28-4 高级搜索条件框 v-show="showAdvanceSearchVisible" -->
<!-- 28-6 添加展开动画效果 <transition name="fade"> 包含整个搜索条件框 </transition> -->
<!-- 30-2 绑定搜索条件数据 v-model="searchValue.xxxxx" -->
<transition name="slide-fade">
<div v-show="showAdvanceSearchVisible"
style="border: 1px solid #379ff5;border-radius: 5px;box-sizing: border-box;padding: 5px;margin: 10px 0;">
<el-row>
<el-col :span="5">
政治面貌:
<el-select v-model="searchValue.politicId" placeholder="请选择政治面貌" size="mini" style="width: 130px;">
<el-option
v-for="item in politicsstatus"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
</el-col>
<el-col :span="4">
民族:
<el-select v-model="searchValue.nationId" placeholder="民族" size="mini" style="width: 130px;">
<el-option
v-for="item in nations"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
</el-col>
<el-col :span="4">
职位:
<el-select v-model="searchValue.posId" placeholder="职位" size="mini" style="width: 130px;">
<el-option
v-for="item in positions"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
</el-col>
<el-col :span="4">
职称:
<el-select v-model="searchValue.jobLevelId" placeholder="职称" size="mini" style="width: 130px;">
<el-option
v-for="item in joblevels"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
</el-col>
<el-col :span="6">
聘用形式:
<el-radio-group v-model="searchValue.engageForm">
<el-radio label="劳动合同">劳动合同</el-radio>
<el-radio label="劳务合同">劳务合同</el-radio>
</el-radio-group>
</el-col>
</el-row>
<el-row style="margin-top: 10px;">
<!-- 30-4 处理部门 v-model="visible2" -->
<el-col :span="5">
所属部门:
<el-popover
placement="bottom"
title="请选择部门"
width="220"
trigger="manual"
v-model="visible2">
<!-- 23-20 添加树形控件 default-expand-all 是否默认展开所有节点 ,节点点击事件 @node-click="handleNodeClick" -->
<el-tree :data="allDeps"
:props="defaultProps"
default-expand-all
@node-click="searchHandleNodeClick"></el-tree>
<!-- 30-6 @node-click="searchHandleNodeClick" -->
<!-- node-click 节点被点击时的回调 共三个参数,依次为:传递给 data 属性的数组中该节点所对应的对象、节点对应的 Node、节点组件本身。 -->
<!-- 自定义点击事件 -->
<!-- 30-7 @click="showDepView2" -->
<div slot="reference"
style="width:130px;display: inline-flex;
border-radius: 5px;border: 1px solid #dedede;height: 28px;cursor: pointer;align-items: center;
font-size: 12px;padding-left: 8px;box-sizing: border-box;"
@click="showDepView2">{
{ inputDepName }}
</div><!-- 23-25 回显数据 {
{inputDepName}} -->
</el-popover>
</el-col>
<!-- 30-3 处理日期:v-model="searchValue.beginDateScope" value-format="yyyy-MM-dd" ;
两个面板各自独立切换当前年份 使用unlink-panels -->
<el-col :span="10">
入职日期:
<el-date-picker
unlink-panels
size="mini"
v-model="searchValue.beginDateScope"
type="datetimerange"
range-separator="至"
value-format="yyyy-MM-dd"
start-placeholder="开始日期"
end-placeholder="结束日期">
</el-date-picker>
</el-col>
<el-col :span="5" :offset="4">
<el-button size="mini">取消</el-button>
<!-- 30-10 @click="initEmps('advanced')" -->
<el-button type="primary" icon="el-icon-search" size="mini" @click="initEmps('advanced')">搜索</el-button>
</el-col>
</el-row>
</div>
</transition>
</div>
<div style="margin-top: 10px;">
<!-- 2、表格;6、添加 loading -->
<el-table
:data="emps"
v-loading="loading"
element-loading-text="拼命加载中"
element-loading-spinner="el-icon-loading"
element-loading-background="rgba(0, 0, 0, 0.8)"
style="width: 100%" stripe border>
<el-table-column
type="selection"
width="55">
</el-table-column>
<el-table-column
prop="name"
label="姓名"
align="left"
fixed
width="90">
</el-table-column>
<el-table-column
prop="gender"
label="性别"
align="left"
width="40">
</el-table-column>
<el-table-column
prop="workId"
label="工号"
align="left"
width="85">
</el-table-column>
<el-table-column
prop="birthday"
label="出生日期"
align="left"
width="95">
</el-table-column>
<el-table-column
prop="idCard"
label="身份证号"
width="150">
</el-table-column>
<el-table-column
prop="wedlock"
label="婚姻状态"
align="center"
width="70">
</el-table-column>
<el-table-column
prop="nation.name"
label="民族"
align="left"
width="50">
</el-table-column>
<el-table-column
prop="nativePlace"
label="籍贯"
align="center"
width="80">
</el-table-column>
<el-table-column
prop="politicsStatus.name"
label="政治面貌"
width="100">
</el-table-column>
<el-table-column
prop="email"
label="电子邮件"
align="left"
width="150">
</el-table-column>
<el-table-column
prop="phone"
label="电话号码"
align="left"
width="100">
</el-table-column>
<el-table-column
prop="address"
label="联系地址"
align="center"
width="220">
</el-table-column>
<el-table-column
prop="department.name"
label="所属部门"
align="left"
width="100">
</el-table-column>
<el-table-column
prop="position.name"
label="职位"
width="100">
</el-table-column>
<el-table-column
prop="joblevel.name"
label="级别"
width="100">
</el-table-column>
<el-table-column
prop="engageForm"
label="聘用形式"
align="left"
width="100">
</el-table-column>
<el-table-column
prop="tiptopDegree"
label="最高学历"
align="center"
width="80">
</el-table-column>
<el-table-column
prop="school"
label="毕业学校"
align="left"
width="150">
</el-table-column>
<el-table-column
prop="specialty"
label="所属专业"
align="left"
width="150">
</el-table-column>
<el-table-column
prop="workState"
label="在职状态"
align="center"
width="70">
</el-table-column>
<el-table-column
prop="beginDate"
label="入职日期"
align="left"
width="95">
</el-table-column>
<el-table-column
prop="conversionTime"
label="转正日期"
align="left"
width="95">
</el-table-column>
<el-table-column
prop="beginContract"
label="合同起始日期"
align="left"
width="95">
</el-table-column>
<el-table-column
prop="endContract"
label="合同截止日期"
align="left"
width="95">
</el-table-column>
<el-table-column
label="合同期限"
align="left"
width="100">
<template slot-scope="scope">
<el-tag>{
{ scope.row.contractTerm }}</el-tag>
年
</template>
</el-table-column>
<el-table-column
label="操作"
fixed="right"
width="200">
<template slot-scope="scope">
<!-- 25-4 给编辑按钮绑定点击事件 @click="showEmpView(scope.row)" -->
<el-button style="padding: 3px;" size="mini" @click="showEmpView(scope.row)">编辑</el-button>
<!-- <el-button style="padding: 3px;" size="mini" type="primary" plain>查看高级资料</el-button> -->
<!-- 24-1 删除员工 @click="deleteEmp(scope.row)" -->
<el-button style="padding: 3px;" size="mini" type="danger" @click="deleteEmp(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 10、分页 -->
<div style="display: flex;justify-content: flex-end;margin-top: 10px;">
<!-- 13、@current-change="currentChange" 当前页
14、@size-change="sizeChange" 每页显示多少条 -->
<el-pagination
prev-text="上一页"
next-text="下一页"
@current-change="currentChange"
@size-change="sizeChange"
:page-sizes="[10,20,30,50,100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total" background>
</el-pagination>
</div>
</div>
<!-- 23-1、开始- 添加员工弹框 -->
<!-- 25-1 编辑员工 将添加员工弹框标题改为变量 根据条件显示是添加还是编辑 :title="title" -->
<el-dialog
:title="title"
:visible.sync="dialogVisible"
width="80%">
<div>
<!-- 23-6、<el-row <el-form -->
<!-- 23-28 数据校验对象 :rules="empRules" ,每项属性对应 prop="posId" -->
<el-form ref="empRef" :model="emp" :rules="empRules">
<el-row>
<el-col :span="6">
<el-form-item label="姓名:" prop="name">
<el-input v-model="emp.name" prefix-icon="el-icon-edit" placeholder="请输入员工姓名" size="mini"
style="width: 150px;"></el-input>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="性别:" prop="gender">
<el-radio-group v-model="emp.gender" style="margin-top: 8px;">
<el-radio label="男">男</el-radio>
<el-radio label="女">女</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="出生日期:" prop="birthday">
<el-date-picker
v-model="emp.birthday"
type="date"
value-format="yyyy-MM-dd"
size="mini"
style="width: 150px;"
placeholder="出生日期">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="7">
<!-- 23-10、 添加员工 给每项赋值 -->
<el-form-item label="政治面貌:" prop="politicId">
<el-select v-model="emp.politicId" placeholder="请选择政治面貌" size="mini" style="width: 200px;">
<el-option
v-for="item in politicsstatus"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="6">
<el-form-item label="民族:" prop="nationId">
<el-select v-model="emp.nationId" placeholder="民族" size="mini" style="width: 150px;">
<el-option
v-for="item in nations"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="籍贯:" prop="nativePlace">
<el-input v-model="emp.nativePlace" placeholder="籍贯" prefix-icon="el-icon-edit" size="small"
style="width: 120px;"></el-input>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="电子邮箱:" prop="email">
<el-input v-model="emp.email" placeholder="请输入电子邮箱" prefix-icon="el-icon-message" size="mini"
style="width: 150px;"></el-input>
</el-form-item>
</el-col>
<el-col :span="7">
<el-form-item label="联系地址:" prop="address">
<el-input v-model="emp.address" placeholder="请输入联系地址" prefix-icon="el-icon-edit" size="mini"
style="width: 200px;"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="6">
<el-form-item label="职位:" prop="posId">
<el-select v-model="emp.posId" placeholder="职位" size="mini" style="width: 150px;">
<el-option
v-for="item in positions"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="职称:" prop="jobLevelId">
<el-select v-model="emp.jobLevelId" placeholder="职称" size="mini" style="width: 150px;">
<el-option
v-for="item in joblevels"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<!-- 23-15 -->
<el-form-item label="所属部门:" prop="departmentId">
<!-- 23-17 manual 手动弹出框 -->
<el-popover
placement="bottom"
title="请选择部门"
width="200"
trigger="manual"
v-model="visible">
<!-- 23-20 添加树形控件 default-expand-all 是否默认展开所有节点 ,节点点击事件 @node-click="handleNodeClick" -->
<el-tree :data="allDeps"
:props="defaultProps"
default-expand-all
@node-click="handleNodeClick"></el-tree>
<!-- node-click 节点被点击时的回调 共三个参数,依次为:传递给 data 属性的数组中该节点所对应的对象、节点对应的 Node、节点组件本身。 -->
<!-- 自定义点击事件 -->
<div slot="reference"
style="width:150px;display: inline-flex;
border-radius: 5px;border: 1px solid #dedede;height: 28px;cursor: pointer;align-items: center;
font-size: 12px;padding-left: 8px;box-sizing: border-box;"
@click="showDepView">{
{ inputDepName }}
</div><!-- 23-25 回显数据 {
{inputDepName}} -->
</el-popover>
</el-form-item>
</el-col>
<el-col :span="7">
<el-form-item label="电话号码:" prop="phone">
<el-input v-model="emp.phone" placeholder="请输入电话号码" size="mini" style="width: 200px;"
prefix-icon="el-icon-phone"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="6">
<el-form-item label="工号:" prop="workId">
<el-input v-model="emp.workId" placeholder="请输入工号" size="mini" style="width: 150px;"
prefix-icon="el-icon-edit" disabled></el-input>
</el-form-item>
</el-col>
<el-col :span="5">
<!-- 23-14 数据在 data 中写死的 -->
<el-form-item label="学历:" prop="tiptopDegree">
<el-select v-model="emp.tiptopDegree" placeholder="职称" size="mini" style="width: 150px;">
<el-option
v-for="item in tiptopDegrees"
:key="item"
:label="item"
:value="item">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="毕业院校:" prop="school">
<el-input v-model="emp.school" placeholder="请输入学校" size="mini" style="width: 150px;"
prefix-icon="el-icon-edit"></el-input>
</el-form-item>
</el-col>
<el-col :span="7">
<el-form-item label="专业名称:" prop="specialty">
<el-input v-model="emp.specialty" placeholder="请输入专业名称" size="mini" style="width: 200px;"
prefix-icon="el-icon-edit"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="6">
<el-form-item label="入职日期:" prop="beginDate">
<el-date-picker
v-model="emp.beginDate"
type="date"
value-format="yyyy-MM-dd"
size="mini"
style="width: 120px;"
placeholder="入职日期">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="转正日期:" prop="conversionTime">
<el-date-picker
v-model="emp.conversionTime"
type="date"
value-format="yyyy-MM-dd"
size="mini"
style="width: 120px;"
placeholder="转正日期">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="合同起始日期:" prop="beginContract">
<el-date-picker
v-model="emp.beginContract"
type="date"
value-format="yyyy-MM-dd"
size="mini"
style="width: 135px;"
placeholder="合同起始日期">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="7">
<el-form-item label="合同截止日期:" prop="endContract">
<el-date-picker
v-model="emp.endContract"
type="date"
value-format="yyyy-MM-dd"
size="mini"
style="width: 170px;"
placeholder="合同截止日期">
</el-date-picker>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="8">
<el-form-item label="身份证号码:" prop="idCard">
<el-input v-model="emp.idCard" placeholder="请输入身份证号码"
size="mini" prefix-icon="el-icon-edit" style="width: 180px;"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="聘用形式:" prop="engageForm">
<el-radio-group v-model="emp.engageForm" style="margin-top: 8px;">
<el-radio label="劳动合同">劳动合同</el-radio>
<el-radio label="劳务合同">劳务合同</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="婚姻状况:" prop="wedlock">
<el-radio-group v-model="emp.wedlock" style="margin-top: 8px;">
<el-radio label="未婚">未婚</el-radio>
<el-radio label="已婚">已婚</el-radio>
<el-radio label="离异">离异</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<!-- 23-26 @click="doAddEmp"-->
<el-button type="primary" @click="doAddEmp">确 定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
export default {
name: "EmpBasic",
data() {
return {
searchValue: { // 30-1 高级搜索 条件对象
politicId: null, // 政治面貌
nationId: null, // 民族
posId: null, // 职位
jobLevelId: null, // 职称
engageForm: '', // 聘用形式
departmentId: null, // 部门 id
beginDateScope: null // 入职日期范围
},
showAdvanceSearchVisible: false, // 28-2 高级搜索框 动态效果
headers: { // 27-12 定义请求头
Authorization: window.sessionStorage.getItem('tokenStr')
},
importDataDisabled: false, // 27-9 导入按钮 默认不禁用
importDataBtnText: '导入数据', // 27-2 导入数据
importDataBtnIcon: 'el-icon-upload2', // 27-2 导入数据
title: '', // 25-2 添加编辑员工弹框动态标题
emps: [], // 3、获取所有员工(分页)
loading: false, // 7、添加 loading
total: 0, // 11 分页总条数
currentPage: 1, // 14、默认显示第1页(currentPage 后端字段)
size: 10, // 15、默认每页显示 10 条
empName: '', // 18、搜索
dialogVisible: false, // 23-2、添加员工弹框
nations: [], // 23-7 添加员工 民族
joblevels: [], // 23-7 职称
politicsstatus: [], // 23-7 政治面貌
positions: [], // 23-7 职位
department: [], // 部门
// 23-13、学历
tiptopDegrees: ['博士', '硕士', '本科', '大专', '高中', '初中', '小学', '其它'],
// 23-5、添加员工
emp: {
id: null,
name: '',
gender: '',
birthday: '',
idCard: '',
wedlock: '',
nationId: null,
nativePlace: '',
politicId: null,
email: '',
phone: '',
address: '',
departmentId: null,
jobLevelId: null,
posId: null,
engageForm: '',
tiptopDegree: '',
specialty: '',
school: '',
beginDate: '',
workState: '在职',
workId: '',
contractTerm: null,
conversionTime: '',
notworkDate: null,
beginContract: '',
endContract: '',
workAge: null,
salaryId: null
},
visible: false, // 23-18 弹出框
visible2: false, // 30-5 高级搜索 部门
// 23-21 树形控件
defaultProps: {
children: 'children',
label: 'name'
},
allDeps: [], // 23-21 树形控件 绑定 所属部门 数据对象
inputDepName: '',// 23-23 回显部门数据
// 23-30 表单数据校验
empRules: {
name: [{required: true, message: '请输入员工名', trigger: 'blur'}],
gender: [{required: true, message: '请输入员工性别', trigger: 'blur'}],
birthday: [{required: true, message: '请输入出生日期', trigger: 'blur'}],
idCard: [{required: true, message: '请输入身份证号码', trigger: 'blur'},
{
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}$)/,
message: '身份证号码不正确', trigger: 'blur'
}],
wedlock: [{required: true, message: '请输入婚姻状况', trigger: 'blur'}],
nationId: [{required: true, message: '请输入民族', trigger: 'blur'}],
nativePlace: [{required: true, message: '请输入籍贯', trigger: 'blur'}],
politicId: [{required: true, message: '请输入政治面貌', trigger: 'blur'}],
email: [{required: true, message: '请输入邮箱地址', trigger: 'blur'},
{type: 'email', message: '邮箱地址格式不正确', trigger: 'blur'}],
phone: [{required: true, message: '请输入电话号码', trigger: 'blur'},
{
pattern: /^(0|86|17951)?(13[0-9]|15[012356789]|17[678]|18[0-9]|14[57])[0-9]{8}$/,
message: '请输入合法手机号码', trigger: 'blur'
}],
address: [{required: true, message: '请输入地址', trigger: 'blur'}],
departmentId: [{required: true, message: '请输入部门名称', trigger: 'blur'}],
jobLevelId: [{required: true, message: '请输入职称', trigger: 'blur'}],
posId: [{required: true, message: '请输入职位', trigger: 'blur'}],
engageForm: [{required: true, message: '请输入聘用形式', trigger: 'blur'}],
tiptopDegree: [{required: true, message: '请输入学历', trigger: 'blur'}],
specialty: [{required: true, message: '请输入专业', trigger: 'blur'}],
school: [{required: true, message: '请输入毕业院校', trigger: 'blur'}],
beginDate: [{required: true, message: '请输入入职日期', trigger: 'blur'}],
workState: [{required: true, message: '请输入工作状态', trigger: 'blur'}],
workId: [{required: true, message: '请输入工号', trigger: 'blur'}],
contractTerm: [{required: true, message: '请输入合同期限', trigger: 'blur'}],
conversionTime: [{required: true, message: '请输入转正日期', trigger: 'blur'}],
notworkDate: [{required: true, message: '请输入离职日期', trigger: 'blur'}],
beginContract: [{required: true, message: '请输入合同起始日期', trigger: 'blur'}],
endContract: [{required: true, message: '请输入合同结束日期', trigger: 'blur'}],
workAge: [{required: true, message: '请输入工龄', trigger: 'blur'}]
}
}
},
mounted() {
this.initEmps() // 5、获取所有员工(分页)
this.initData() // 23-9 添加员工
this.initPositions() // 23-12 获取职位
},
methods: {
// 27-6 数据导入成功 恢复原来的图标和状态
onSuccess() {
this.importDataBtnIcon = 'el-icon-upload2'
this.importDataBtnText = '导入数据'
this.importDataDisabled = false // 29-10 不禁用导入按钮
this.initEmps()
},
// 27-7 数据导入失败 恢复原来的图标和状态
onError() {
this.importDataBtnIcon = 'el-icon-upload2'
this.importDataBtnText = '导入数据'
this.importDataDisabled = false // 29-10 不禁用导入按钮
},
// 27-4、导入数据 改变图标和添加 loading 状态
beforeUpload() {
this.importDataBtnIcon = 'el-icon-loading'
this.importDataBtnText = '正在导入'
this.importDataDisabled = true // 29-10 禁用导入按钮
},
// 26-2 下载请求
exportData() {
this.downloadRequest('/employee/basic/export')
},
// 25-5 编辑员工按钮 点击事件
showEmpView(data) {
this.title = '编辑员工信息'
this.emp = data // 回显数据
this.inputDepName = data.department.name // 25-7 回显部门信息
this.initPositions() // 25-9 初始化职位信息
this.dialogVisible = true
},
// 24-2 删除员工
deleteEmp(data) {
this.$confirm('此操作将永久删除该员工' + data.name + ', 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.deleteRequest('/employee/basic/' + data.id).then(resp => {
if (resp) {
this.initEmps()
}
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
},
// 23-27 确定添加员工
// 25-10 添加或编辑员工 有id编辑员工 没有id添加员工
// 添加和编辑这里就请求方式不一样 putRequest postRequest ,其它的都一样
doAddEmp() {
if (this.emp.id) {
// 有 id 编辑员工
this.$refs['empRef'].validate(valid => {
if (valid) {
this.putRequest('/employee/basic/', this.emp).then(resp => {
if (resp) {
this.dialogVisible = false
this.initEmps()
}
})
}
})
} else {
// 没有id 添加员工
// empRef 表单中定义的引用对象 ref="empRef"
this.$refs['empRef'].validate(valid => {
if (valid) {
this.postRequest('/employee/basic/', this.emp).then(resp => {
if (resp) {
this.dialogVisible = false
this.initEmps()
}
})
}
})
}
},
// 30-7 高级搜索 部门点击事件
searchHandleNodeClick(data) {
this.inputDepName = data.name
this.searchValue.departmentId = data.id
this.visible2 = !this.visible2 // 弹框
},
// 23-22、24 树控件节点点击事件
handleNodeClick(data) {
this.inputDepName = data.name
this.emp.departmentId = data.id
this.visible = !this.visible // 弹框
},
// 30-9 高级搜索 部门弹框
showDepView2() {
this.visible2 = !this.visible2
},
// 23-16 添加员工 所属部门
showDepView() {
this.visible = !this.visible // 23-19 弹出框
},
// 23-13 添加员工 获取最大号
getMaxworkId() {
this.getRequest('/employee/basic/maxWorkID').then(resp => {
if (resp) {
this.emp.workId = resp.obj
}
})
},
// 23-11、 添加员工 获取职位 有可能变动 打开对话框的时候调用此方法
initPositions() {
this.getRequest('/employee/basic/Positions').then(resp => {
if (resp) {
this.positions = resp
}
})
},
// 23-8、添加员工 不怎么变动的数据。放 sessionStorage ,就不用怎么去查
initData() {
// 获取民族数据:先从 sessionStorage 里取,取不到再调用接口获取数据
if (!window.sessionStorage.getItem("nations")) {
this.getRequest('/employee/basic/nations').then(resp => {
this.nations = resp
// 存到 sessionStorage 里,把对象转字符串
window.sessionStorage.setItem('nations', JSON.stringify(resp))
})
} else {
// 从 sessionStorage 获取,字符串转对象
this.nations = JSON.parse(window.sessionStorage.getItem('nations'))
}
// 获取职称
if (!window.sessionStorage.getItem('joblevels')) {
this.getRequest('/employee/basic/joblevels').then(resp => {
if (resp) {
this.joblevels = resp
window.sessionStorage.setItem('joblevels', JSON.stringify(resp))
}
})
} else {
// 从 sessionStorage 获取,字符串转对象
this.joblevels = JSON.parse(window.sessionStorage.getItem('joblevels'))
}
// 获取政治面貌
if (!window.sessionStorage.getItem('politicsstatus')) {
this.getRequest('/employee/basic/politicsStatus').then(resp => {
if (resp) {
this.politicsstatus = resp
window.sessionStorage.setItem('politicsstatus', JSON.stringify(resp))
}
})
} else {
// 从 sessionStorage 获取,字符串转对象
this.politicsstatus = JSON.parse(window.sessionStorage.getItem('politicsstatus'))
}
// 23-22 树形控件 绑定 所属部门 数据对象
if (!window.sessionStorage.getItem('allDeps')) {
this.getRequest('/employee/basic/deps').then(resp => {
if (resp) {
this.allDeps = resp
window.sessionStorage.setItem('allDeps', JSON.parse(resp))
}
})
} else {
this.allDeps = window.sessionStorage.getItem('allDeps')
}
},
// 23-4、添加员点击事件
showAddEmpView() {
// 25-6 清空表单
this.emp = {
id: null,
name: '',
gender: '',
birthday: '',
idCard: '',
wedlock: '',
nationId: null,
nativePlace: '',
politicId: null,
email: '',
phone: '',
address: '',
departmentId: null,
jobLevelId: null,
posId: null,
engageForm: '',
tiptopDegree: '',
specialty: '',
school: '',
beginDate: '',
workState: '在职',
workId: '',
contractTerm: null,
conversionTime: '',
notworkDate: null,
beginContract: '',
endContract: '',
workAge: null,
salaryId: null
}
this.inputDepName = '' // 25-8 清空部门信息
this.title = '添加员工' // 25-3 点击添加员工按钮时,弹出框标题为 添加员工
this.getMaxworkId() // 23-14 获取最大工号
this.initPositions() // 23-12 获取职位
this.dialogVisible = true
},
// 15、分页 每页显示多少条 默认会把 size 传进来
sizeChange(size) {
this.size = size
this.initEmps()
},
// 13、分页-当前页-currentPage 点击的时候自己会带过来
currentChange(currentPage) {
this.currentPage = currentPage // 16
this.initEmps() // 18、调用方法
},
// 4、获取所有员工(分页)
initEmps(type) {
this.loading = true // 8、添加 loading
// 30-11 定义高级搜索 url
let url = '/employee/basic/?currentPage=' + this.currentPage + '&size=' + this.size
if (type && type === 'advanced') { // 说明是高级搜索
if (this.searchValue.politicId) {
url += '&politicId=' + this.searchValue.politicId
}
if (this.searchValue.nationId) {
url += '&nationId=' + this.searchValue.nationId
}
if (this.searchValue.posId) {
url += '&posId=' + this.searchValue.posId
}
if (this.searchValue.jobLevelId) {
url += '&jobLevelId=' + this.searchValue.jobLevelId
}
if (this.searchValue.engageForm) {
url += '&engageForm=' + this.searchValue.engageForm
}
if (this.searchValue.departmentId) {
url += '&departmentId=' + this.searchValue.departmentId
}
if (this.searchValue.beginDateScope) {
url += '&beginDateScope=' + this.searchValue.beginDateScope
}
} else {
url += '&name=' + this.empName
}
// 17、添加分页参数 ?currentPage='+this.currentPage+'&size='+this.size
// 19、添加用户名搜索参数 +'&name='+this.empName,传参 根据条件搜索,不传参查询所有
this.getRequest(url).then(resp => {
// this.getRequest('/employee/basic/').then(resp => {
this.loading = false // 9、关闭 loading
if (resp) {
this.emps = resp.data
this.total = resp.total // 12、分页
}
});
}
}
}
</script>
<style>
/*28-7 展开收起条件搜索框动画样式 */
/* 可以设置不同的进入和离开动画 */
/* 设置持续时间和动画函数 */
.slide-fade-enter-active {
transition: all .8s ease;
}
.slide-fade-leave-active {
transition: all .8s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}
.slide-fade-enter, .slide-fade-leave-to
/* .slide-fade-leave-active for below version 2.1.8 */
{
transform: translateX(10px);
opacity: 0;
}
</style>
十、工资账套管理
SalSob.vue
<template>
<div>
<!-- 1-1 绘制表格 -->
<div style="display: flex;justify-content: space-between;">
<!-- 2-3 @click="showAddSalaryView" 点击打开 添加工资账套对话框 -->
<el-button type="primary" icon="el-icon-plus" size="mini" @click="showAddSalaryView">添加工资账套</el-button>
<!-- 5-3 刷新功能 直接绑定点击事件 调用获取所有数据方法 -->
<el-button type="success" icon="el-icon-refresh" size="mini" @click="initSalaries"></el-button>
</div>
<div style="margin-top: 10px;">
<el-table
:data="salaries"
stripe
border>
<!-- 多选框 type="selection" -->
<el-table-column
type="selection"
width="40">
</el-table-column>
<el-table-column
prop="name"
label="账套名称"
width="120">
</el-table-column>
<el-table-column
prop="basicSalary"
label="基本工资"
width="70">
</el-table-column>
<el-table-column
prop="trafficSalary"
label="交通补助"
width="70">
</el-table-column>
<el-table-column
prop="lunchSalary"
label="午餐补助"
width="70">
</el-table-column>
<el-table-column
prop="bonus"
label="奖金"
width="70">
</el-table-column>
<el-table-column
prop="createDate"
label="启用时间"
width="100">
</el-table-column>
<!-- 多级表头:el-table-column 里面嵌套 el-table-column,就可以实现多级表头 -->
<el-table-column
label="养老金"
align="center">
<el-table-column
prop="pensionPer"
label="比率"
width="70">
</el-table-column>
<el-table-column
prop="pensionBase"
label="基数"
width="70">
</el-table-column>
</el-table-column>
<!-- 多级表头 -->
<el-table-column
label="医疗保险"
align="center">
<el-table-column
prop="medicalPer"
label="比率"
width="70">
</el-table-column>
<el-table-column
prop="medicalBase"
label="基数"
width="70">
</el-table-column>
</el-table-column>
<!-- 多级表头 -->
<el-table-column
label="公积金"
align="center">
<el-table-column
prop="accumulationFundPer"
label="比率"
width="70">
</el-table-column>
<el-table-column
prop="accumulationFundBase"
label="基数"
width="70">
</el-table-column>
</el-table-column>
<el-table-column
label="操作">
<!-- 5-1 删除工资账套 拿到当前行数据 绑定点击事件 传行数据-->
<template slot-scope="scope">
<!-- 6-4 @click="showEditSalaryView(scope.row)">编辑 -->
<el-button type="primary" size="mini" @click="showEditSalaryView(scope.row)">编辑</el-button>
<el-button type="danger" size="mini" @click="deleteSalary(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 2-1 添加工资账套对话框 -->
<!-- 6-2 把标题变成属性 -->
<el-dialog
:title="dialogTitle"
:visible.sync="dialogVisible"
width="50%">
<!-- 3-8 调整样式 -->
<div style="display: flex;justify-content: space-around;align-items: center;">
<!-- 3-1 添加步骤条 -->
<!-- 3-5 :active="activeItemIndex" -->
<el-steps direction="vertical" :active="activeItemIndex">
<!-- 3-3 循环遍历数据 -->
<el-step :title="itemName" v-for="(itemName,index) in salaryItemName" :key="index"></el-step>
</el-steps>
<!-- 3-4 循环遍历数据 -->
<!-- 3-7 v-show="activeItemIndex = index" 与下标相等才展示 -->
<!-- 4-2 修改各项的值 绑定和遍历-->
<el-input v-model="salary[title]" :placeholder="'请输入'+salaryItemName[index]+'...'"
v-for="(value,title,index) in salary"
:key="index" v-show="activeItemIndex === index" style="width: 200px;"></el-input>
</div>
<span slot="footer" class="dialog-footer">
<!-- 3-10 按钮判断根据索引显示 文字提示 -->
<el-button @click="preStep">{
{ activeItemIndex === 10 ? '取消' : '上一步' }}</el-button>
<el-button type="primary" @click="nextStep">{
{ activeItemIndex === 10 ? '完成' : '下一步' }}</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
export default {
name: "SalSob",
data() {
return {
dialogTitle: '添加工资账套', // 6-1 标题
dialogVisible: false, // 2-2 添加工资账套对话框
salaries: [], // 1-2 定义数组
activeItemIndex: 0, // 3-6 步骤条激活索引
salaryItemName: [ // 3-2 步骤条数据对象
'账套名称',
'基本工资',
'交通补助',
'午餐补助',
'奖金',
'养老金比率',
'养老金基数',
'医疗保险比率',
'医疗保险基数',
'公积金比率',
'公积金基数'
],
// 4-1 定义工资账套数据
salary: {
name: '',
basicSalary: 0,
trafficSalary: 0,
lunchSalary: 0,
bonus: 0,
pensionPer: 0.0,
pensionBase: 0,
medicalPer: 0.0,
medicalBase: 0,
accumulationFundPer: 0.0,
accumulationFundBase: 0
}
}
},
mounted() {
this.initSalaries()
},
methods: {
// 6-5 点击编辑显示对话框
showEditSalaryView(data) {
this.dialogTitle = '编辑工资账套' // 设置标题
this.activeItemIndex = 0 // 默认激活的索引
this.salary.id = data.id
this.salary.name = data.name
this.salary.basicSalary = data.basicSalary
this.salary.trafficSalary = data.trafficSalary
this.salary.lunchSalary = data.lunchSalary
this.salary.bonus = data.bonus
this.salary.pensionPer = data.pensionPer
this.salary.pensionBase = data.pensionBase
this.salary.medicalPer = data.medicalPer
this.salary.medicalBase = data.medicalBase
this.salary.accumulationFundPer = data.accumulationFundPer
this.salary.accumulationFundBase = data.accumulationFundBase
this.dialogVisible = true // 打开对话框
},
// 5-2 删除工资账套
deleteSalary(data) {
this.$confirm('此操作将永久删除该[' + data.name + ']工资账套, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.deleteRequest('/salary/sob/' + data.id).then(resp => {
if (resp) {
this.initSalaries()
}
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
},
preStep() { // 3-13 上一步 取消
if (this.activeItemIndex === 0) {
return
} else if (this.activeItemIndex === 10) {
this.dialogVisible = false;
return;
}
this.activeItemIndex--
},
nextStep() { // 3-12 下一步 完成
if (this.activeItemIndex === 10) {
// alert("ok")
// console.log(this.salary)
// 4-4 添加工资账套
if (this.salary.id) { // 6-6 有 id 调用编辑接口,没有 id 执行添加
this.putRequest('/salary/sob/', this.salary).then(resp => {
if (resp) {
this.initSalaries()
this.dialogVisible = false // 关闭弹框
}
})
} else {
this.postRequest('/salary/sob/', this.salary).then(resp => {
if (resp) {
this.initSalaries()
this.dialogVisible = false
}
})
}
return
}
this.activeItemIndex++
},
// 2-4 点击打开添加工资账套对话框
showAddSalaryView() {
this.dialogTitle = '添加工资账套' // 6-3 添加的时候显示此标题
this.salary = { // 4-3 清空表单
name: '',
basicSalary: 0,
trafficSalary: 0,
lunchSalary: 0,
bonus: 0,
pensionPer: 0.0,
pensionBase: 0,
medicalPer: 0.0,
medicalBase: 0,
accumulationFundPer: 0.0,
accumulationFundBase: 0
}
this.activeItemIndex = 0 // 3-14 步骤条索引从0开始
this.dialogVisible = true;
},
// 1-3 初始化数据
initSalaries() {
this.getRequest('/salary/sob/').then(resp => {
if (resp) {
this.salaries = resp
}
})
}
}
}
</script>
<style scoped>
</style>
十一、员工账套设置
SalSobCfg.vue
<template>
<div>
<el-table
size="mini"
:data="emps"
stripe
border>
<el-table-column
align="left"
type="selection"
width="55">
</el-table-column>
<el-table-column
prop="name"
label="姓名"
align="left"
fixed
width="120">
</el-table-column>
<el-table-column
prop="workId"
label="工号"
align="left"
width="120">
</el-table-column>
<el-table-column
prop="email"
label="邮箱地址"
align="left"
width="200">
</el-table-column>
<el-table-column
prop="phone"
label="电话号码"
align="left"
width="120">
</el-table-column>
<el-table-column
prop="department.name"
label="所属部门"
align="left"
width="120">
</el-table-column>
<el-table-column
label="工资账套"
align="center">
<template slot-scope="scope">
<el-tooltip placement="right" v-if="scope.row.salary">
<div slot="content">
<table>
<tr>
<td>基本工资</td>
<td>{
{ scope.row.salary.basicSalary }}</td>
</tr>
<tr>
<td>交通补助</td>
<td>{
{ scope.row.salary.trafficSalary }}</td>
</tr>
<tr>
<td>午餐补助</td>
<td>{
{ scope.row.salary.lunchSalary }}</td>
</tr>
<tr>
<td>奖金</td>
<td>{
{ scope.row.salary.bonus }}</td>
</tr>
<tr>
<td>养老金比率</td>
<td>{
{ scope.row.salary.pensionPer }}</td>
</tr>
<tr>
<td>养老金基数</td>
<td>{
{ scope.row.salary.pensionBase }}</td>
</tr>
<tr>
<td>医疗保险比率</td>
<td>{
{ scope.row.salary.medicalPer }}</td>
</tr>
<tr>
<td>医疗保险基数</td>
<td>{
{ scope.row.salary.medicalBase }}</td>
</tr>
<tr>
<td>公积金比率</td>
<td>{
{ scope.row.salary.accumulationFundPer }}</td>
</tr>
<tr>
<td>公积金基数</td>
<td>{
{ scope.row.salary.accumulationFundBase }}</td>
</tr>
</table>
</div>
<el-tag>{
{ scope.row.salary.name }}</el-tag>
</el-tooltip>
<el-tag v-else>暂未设置</el-tag>
</template>
</el-table-column>
<!-- 2-1 编辑工资账套 -->
<el-table-column
label="操作"
align="center">
<template slot-scope="scope">
<!-- 2-5 当前员工的工资账套 @show="showPop(scope.row.salary)" show 显示时触发 -->
<!-- 2-9 @hide="hidePop(scope.row)" hide 隐藏时触发 -->
<el-popover
size="mini"
@show="showPop(scope.row.salary)"
@hide="hidePop(scope.row)"
placement="right"
title="编辑工资账套"
width="200"
trigger="click">
<div>
<!-- 2-6 v-model="currentSalary" -->
<el-select v-model="currentSalary" placeholder="请选择">
<el-option
size="mini"
v-for="item in salaries"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
</div>
<el-button slot="reference" type="danger">修改工资账套</el-button>
</el-popover>
</template>
</el-table-column>
</el-table>
<!-- 1-1 分页组件 -->
<div style="display: flex;justify-content: flex-end;margin-top: 5px;">
<el-pagination
@size-change="sizeChange"
@current-change="currentChange"
layout="total, sizes, prev, pager, next, jumper"
:total="total" background>
</el-pagination>
</div>
</div>
</template>
<script>
export default {
name: "SalSobCfg",
data() {
return {
emps: [],
salaries: [], // 2-2 工资账套数组
currentPage: 1, // 1-2 当前页
size: 10, // 1-2 每页显示条数
total: 0, // 1-2 分页
currentSalary: null // 2-7 当前员工工资账套
}
},
mounted() {
this.initEmps()
this.initSalaries() // 2-4 初始化 获取所有工资账套
},
methods: {
// 2-10
hidePop(data) { // 隐藏时触发
// 当前员工工资账套存在 并且不等于当前的 才更新
if (this.currentSalary && this.currentSalary!==data.salary.id) {
this.putRequest('/salary/sobcfg/?eid=' + data.id + '&sid=' + this.currentSalary).then(resp => {
if (resp) {
this.initEmps()
}
});
}
},
// 2-8 员工工资账套
showPop(data) { // 显示时触发
if (data) {
this.currentSalary = data.id;
} else {
this.currentSalary = null
}
},
// 2-3 获取所有工资账套
initSalaries() {
this.getRequest('/salary/sobcfg/salaries').then(resp => {
if (resp) {
this.salaries = resp
}
})
},
// 1-3 分页-当前页
currentChange(page) {
this.currentPage = page
this.initEmps()
},
// 1-4 分页-每页显示数量
sizeChange(size) {
this.size = size
this.initEmps()
},
// 获取所有数据
initEmps() {
this.getRequest('/salary/sobcfg/?currentPage=' + this.currentPage + '&size=' + this.size).then(resp => {
if (resp) {
this.emps = resp.data
this.total = resp.total
}
})
}
}
}
</script>
<style scoped>
</style>
十二、聊天功能
将GitHub上的开源项目与自身项目进行整合来实现即时聊天功能
一个基于Vue + Webpack构建的简单chat示例,聊天记录保存在localStorge。简单演示了Vue的 component、filter、directive、computed以及组件间的事件通讯。 原项目目前存在一个Bug:打开项目关闭浏览器再次打开会报错。这里使用在此项目基础上重构的项目 来与我们项目进行整合.
https://github.com/is-liyiwei/vue-Chat-demo
整合项目
assets:是网页所需要的图片,这个不需要,因为后端提供了图片,更改代码直接从后端获取。
componts:在自己的目录下创建该目录下的几个文件,复制过去。
vuex:是stroy,将其中的的代码,加入到本项目的story/index.js
main.js:就是一个普通的入口,不需要加入项目当汇总。
components
card.vue
<template>
<div id="card">
<header>
<img class="avatar" v-bind:src="user.userFace" v-bind:alt="user.name">
<p class="name">{
{user.name}}</p>
</header>
<footer>
<input class="search" type="text" v-model="$store.state.filterKey" placeholder="search user...">
</footer>
</div>
</template>
<script>
export default {
name: 'card',
data () {
return {
user:JSON.parse(window.sessionStorage.getItem("user"))
}
}
}
</script>
<style lang="scss" scoped>
#card {
padding: 12px;
.avatar{
width: 40px;
height: 40px;
vertical-align: middle;/*这个是图片和文字居中对齐*/
}
.name {
display: inline-block;
padding: 10px;
margin-bottom: 15px;
font-size: 16px;
}
.search {
background: #26292E;
height: 30px;
line-height: 30px;
padding: 0 10px;
border: 1px solid #3a3a3a;
border-radius: 4px;
outline: none;/*鼠标点击后不会出现蓝色边框*/
color: #FFF;
}
}
</style>
list.vue
<template>
<div id="list">
<ul style="padding-left: 0;">
<li v-for="item in admins" :class="{ active: currentSession?item.username === currentSession.username:false }"
v-on:click="changecurrentSession(item)"><!-- :class="[item.id === currentSession ? 'active':'']" -->
<!-- 未读消息提示 小红点 <el-badge is-dot> </el-badge> -->
<el-badge is-dot :is-dot="idDot[user.username+'#'+item.username]"><img class="avatar" :src="item.userFace" :alt="item.name"></el-badge>
<p class="name">{
{ item.name }}</p>
</li>
</ul>
</div>
</template>
<script>
import {mapState} from 'vuex'
export default {
name: 'list',
data() {
return {
user:JSON.parse(window.sessionStorage.getItem('user'))
}
},
computed: mapState([
'idDot',
'admins',
'currentSession'
]),
methods: {
changecurrentSession: function (currentSession) {
this.$store.commit('changecurrentSession', currentSession)
}
}
}
</script>
<style lang="scss" scoped>
#list {
li {
padding: 0 15px;
border-bottom: 1px solid #292C33;
cursor: pointer;
&:hover {
background-color: rgba(255, 255, 255, 0.03);
}
}
li.active { /*注意这个是.不是冒号:*/
background-color: rgba(255, 255, 255, 0.1);
}
.avatar {
border-radius: 2px;
width: 30px;
height: 30px;
vertical-align: middle;
}
.name {
display: inline-block;
margin-left: 15px;
}
}
</style>
message.vue
<template>
<div id="message" v-scroll-bottom="sessions">
<ul v-if="currentSession">
<li v-for="entry in sessions[user.username+'#'+currentSession.username]">
<p class="time">
<span>{
{ entry.date | time }}</span>
</p>
<div class="main" :class="{self:entry.self}">
<img class="avatar" :src="entry.self ? user.userFace:currentSession.userFace" alt="">
<p class="text">{
{ entry.content }}</p>
</div>
</li>
</ul>
</div>
</template>
<script>
import {mapState} from 'vuex'
export default {
name: 'message',
data() {
return {
user: JSON.parse(window.sessionStorage.getItem('user')), // 当前用户
}
},
computed: mapState([
'sessions',
'currentSession'
]),
filters: {
time(date) {
if (date) {
date = new Date(date);
}
return `${date.getHours()}:${date.getMinutes()}`;
}
},
directives: {/*这个是vue的自定义指令,官方文档有详细说明*/
// 发送消息后滚动到底部,这里无法使用原作者的方法,也未找到合理的方法解决,暂用setTimeout的方法模拟
'scroll-bottom'(el) {
//console.log(el.scrollTop);
setTimeout(function () {
el.scrollTop += 9999;
}, 1)
}
}
}
</script>
<style lang="scss" scoped>
#message {
padding: 15px;
max-height: 68%;
overflow-y: scroll;
ul {
list-style-type: none;
padding-left: 0;
li {
margin-bottom: 15px;
}
}
.time {
text-align: center;
margin: 7px 0;
> span {
display: inline-block;
padding: 0 18px;
font-size: 12px;
color: #FFF;
background-color: #dcdcdc;
border-radius: 2px;
}
}
.main {
.avatar {
float: left;
margin: 0 10px 0 0;
border-radius: 3px;
width: 30px;
height: 30px;
}
.text {
display: inline-block;
padding: 0 10px;
max-width: 80%;
background-color: #fafafa;
border-radius: 4px;
line-height: 30px;
}
}
.self {
text-align: right;
.avatar {
float: right;
margin: 0 0 0 10px;
border-radius: 3px;
width: 30px;
height: 30px;
}
.text {
display: inline-block;
padding: 0 10px;
max-width: 80%;
background-color: #b2e281;
border-radius: 4px;
line-height: 30px;
}
}
}
</style>
usertext.vue
<template>
<div id="uesrtext">
<textarea placeholder="按 Ctrl + Enter 发送" v-model="content" v-on:keyup="addMessage"></textarea>
</div>
</template>
<script>
import {mapState} from 'vuex'
export default {
name: 'uesrtext',
data() {
return {
content: ''
}
},
computed: mapState([
'currentSession'
]),
methods: {
addMessage(e) {
if (e.ctrlKey && e.keyCode === 13 && this.content.length) {
// 自定义发送消息
let msgObj = {}
// let msgObj = new Object()
msgObj.to = this.currentSession.username
msgObj.content = this.content
this.$store.state.stomp.send('/ws/chat', {}, JSON.stringify(msgObj))
this.$store.commit('addMessage', msgObj);
this.content = '';
}
}
}
}
</script>
<style lang="scss" scoped>
#uesrtext {
position: absolute;
bottom: 0;
right: 0;
width: 100%;
height: 30%;
border-top: solid 1px #DDD;
> textarea {
padding: 10px;
width: 100%;
height: 100%;
border: none;
outline: none;
}
}
</style>
chat/FriendChat.vue
<template>
<div id="app">
<div class="sidebar">
<card></card>
<list></list>
</div>
<div class="main">
<message></message>
<userText></userText>
</div>
</div>
</template>
<script>
import card from '@/components/chat/card.vue'
import list from '@/components/chat/list.vue'
import message from '@/components/chat/message.vue'
import userText from '@/components/chat/usertext.vue'
export default {
name: 'FriendChat',
data () {
return {
}
},
mounted:function() {
this.$store.dispatch('initData');
},
components:{
card,
list,
message,
userText
}
}
</script>
<style lang="scss" scoped>
#app {
margin: 20px 100px;
//margin: 20px auto;
width: 800px;
height: 600px;
overflow: hidden;
border-radius: 10px;
border: 1px solid #c8c9c9;
.sidebar, .main {
height: 100%;
}
.sidebar {
float: left;
color: #f4f4f4;
background-color: #2e3238;
width: 200px;
}
.main {
position: relative;
overflow: hidden;
background-color: #eee;
}
}
</style>
十三、个人中心
AdminInfo.vue
<template>
<div>
<el-card class="box-card" style="width: 400px;">
<div slot="header" class="clearfix">
<span>{
{ admin.name }}</span>
</div>
<div>
<div>
<div style="display: flex;justify-content: center;">
<img title="点击修改用户头像" :src="admin.userFace" style="height: 100px;width: 100px;border-radius: 50px;" alt="">
</div>
<div>电话号码:
<el-tag>{
{ admin.telephone }}</el-tag>
</div>
<div>手机号码:
<el-tag>{
{ admin.phone }}</el-tag>
</div>
<div>居住地址:
<el-tag>{
{ admin.address }}</el-tag>
</div>
<div>用户标签:
<el-tag type="success" v-for="(r,index) in admin.roles" :key="index">{
{ r.nameZh }}</el-tag>
</div>
</div>
<div style="display: flex;justify-content: space-around;margin-top: 10px;">
<!-- 1-3 @click="showUpdateAdminInfoView" -->
<el-button type="primary" size="mini" @click="showUpdateAdminInfoView">修改信息</el-button>
<!-- 2-1 用户修改密码 @click="showUpdatePasswordView" -->
<el-button type="danger" size="mini" @click="showUpdatePasswordView">修改密码</el-button>
</div>
</div>
</el-card>
<!-- 1-1 编辑用户信息 -->
<el-dialog
title="编辑用户信息"
:visible.sync="dialogVisible"
width="30%">
<div>
<table>
<tr>
<td>用户昵称:</td>
<td>
<!-- 1-5 重新给每项赋值 admin2 -->
<el-input v-model="admin2.name"></el-input>
</td>
</tr>
<tr>
<td>电话号码:</td>
<td>
<el-input v-model="admin2.telephone"></el-input>
</td>
</tr>
<tr>
<td>手机号码:</td>
<td>
<el-input v-model="admin2.phone"></el-input>
</td>
</tr>
<tr>
<td>用户地址:</td>
<td>
<el-input v-model="admin2.address"></el-input>
</td>
</tr>
</table>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<!-- 1-8 @click="updateAdminInfo" -->
<el-button type="primary" @click="updateAdminInfo">确 定</el-button>
</span>
</el-dialog>
<!-- 2-2 修改密码 -->
<el-dialog
title="更新密码"
:visible.sync="passwordDialogVisible"
width="30%">
<div>
<!-- 2-8 调整修改密码表单 -->
<el-form :model="ruleForm" status-icon :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
<el-form-item label="请输入旧密码" prop="oldPass">
<el-input type="password" v-model="ruleForm.oldPass" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="请输入新密码" prop="pass">
<el-input type="password" v-model="ruleForm.pass" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="确认新密码" prop="checkPass">
<el-input type="password" v-model="ruleForm.checkPass" autocomplete="off"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('ruleForm')">提交</el-button>
<el-button @click="resetForm('ruleForm')">重置</el-button>
</el-form-item>
</el-form>
</div>
</el-dialog>
</div>
</template>
<script>
export default {
name: "AdminInfo",
data() {
// 2-5 修改密码校验规则 一定要放最前面
var validatePass = (rule, value, callback) => {
if (value === '') {
callback(new Error('请输入密码'));
} else {
if (this.ruleForm.checkPass !== '') {
this.$refs.ruleForm.validateField('checkPass');
}
callback();
}
}
var validatePass2 = (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入密码'));
} else if (value !== this.ruleForm.pass) {
callback(new Error('两次输入密码不一致!'));
} else {
callback();
}
}
return {
admin: null,
admin2: null, // 1-5 编辑的对象
dialogVisible: false, // 1-2 编辑用户信息
passwordDialogVisible: false, // 2-3 修改密码
ruleForm: { // 2-6 校验对象 规则
pass: '',
checkPass: '',
oldPass: '', // 2-9
},
rules: {
pass: [
{validator: validatePass, trigger: 'blur'}
],
checkPass: [
{validator: validatePass2, trigger: 'blur'}
],
oldPass: [
{validator: validatePass, trigger: 'blur'}
]
}
}
},
mounted() {
this.initAdmin()
},
methods: {
// 2-7 预校验 提交表单
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
// alert('submit!');
this.ruleForm.adminId = this.admin.id
this.putRequest('/admin/pass', this.ruleForm).then(resp => {
if (resp) {
// 更新密码成功后 退出登录
this.postRequest('/logout') // 退出登录
window.sessionStorage.removeItem('user')
window.sessionStorage.removeItem('tokenStr')
this.$store.commit('initRoutes', []) // 初始化路由 菜单 置空
this.$router.replace('/') // 跳到登录页面
}
})
} else {
console.log('error submit!!');
return false;
}
});
},
// 2-7 重围修改密码表单
resetForm(formName) {
this.$refs[formName].resetFields();
},
// 2-4 修改密码
showUpdatePasswordView() {
this.passwordDialogVisible = true
},
// 1-9 更新用户
updateAdminInfo() {
this.putRequest('/admin/info', this.admin2).then(resp => {
if (resp) {
this.dialogVisible = false
this.initAdmin()
}
})
},
// 1-4 编辑用户信息弹框
showUpdateAdminInfoView() {
this.dialogVisible = true
},
initAdmin() {
this.getRequest('/admin/info').then(resp => {
if (resp) {
this.admin = resp
this.admin2 = Object.assign({}, this.admin) // 1-6 对象拷贝给 admin2
window.sessionStorage.setItem('user', JSON.stringify(resp))
this.$store.commit('INIT_ADMIN', resp)
}
})
}
}
}
</script>
<style scoped>
</style>