一、项目简介

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

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

实现方式:本项目基于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),当我开发前端内容的时候,可以把前端的请求通过前端服务器转发给后端(称为反向代理),这样就能实时观察结果,并且不需要知道后端怎么实现,而只需要知道接口提供的功能。

目录

一、项目简介

1.1 技术架构

1.2 前端技术架构

1.3 云E办(前端)

二、Vue.js框架

2.1Vue特性

2.2 MVVM设计模式

2.3 vue相关组件

三、搭建vue.js项目

3.1 搭建vue.js项目

3.2 构建前端项目

3.3 vue项目结构分析

3.4 安装 Element-UI

3.5 安装axios

3.6 安装Vuex

3.7 安装VueRouter

3.8 安装font-awesome

四、前端拦截器

4.1 配置登陆拦截器

4.2 axios请求拦截器request

4.3 axios响应拦截器response

4.4 封装请求

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

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

五、登陆页面

5.1 样式设计

5.2 登陆页功能设计

5.3 Login.vue登录页

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

5.5 前端路由导航守卫

main.js

5.6 解决前后端跨域

前端反向代理

5.7 运行项目

六、首页页面

6.1 菜单功能设计与实现

配置store/index.js

在Main.js中引入store

6.2 封装菜单请求工具

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

menus.js

更新main.js

6.3 样式设计

6.4 Home.vue代码

6.5 更新路由router/index.js

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

6.6 index.html消除边距

七、基础信息设置

7.1 样式设计

Tabs 标签页

7.2 组件化开发

7.3 SysBasic.vue

7.4 部门管理DepMana.vue组件

7.5 职位管理PosMana.vue组件

PosMana.vue

7.6 职称管理JobLevelMana.vue组件

7.7 权限组PositionMana.vue组件

八、操作员管理

SysAdmin.vue

九、员工资料

EmpBasic.vue

十、工资账套管理

SalSob.vue

十一、员工账套设置

SalSobCfg.vue

十二、聊天功能

整合项目

components

card.vue

list.vue

message.vue

usertext.vue

十三、个人中心

AdminInfo.vue

十四、源代码


二、Vue.js框架

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

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

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

(3)将项目进行模块化;

(4)解决功能复用和变更问题;

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

(6)解决发布流程问题。

为了解决上述问题便产生了工程化的思想,工程化就是这种避免重复造轮子的最好实践。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特性

虚拟DOM

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

响应式

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

组件化

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

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

局部刷新

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

2.2 MVVM设计模式

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

2.3 vue相关组件

脚手架vue-cli

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

路由Vue-router

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

状态管理vuex

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

通信框架axios

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

(1)从浏览器中创建XMLHttpRequest

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

(3)支持PromiseApi

(4)拦截请求和响应

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

(6)取消请求

(7)自动转换JSON数据

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

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

打包工具webpack

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

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

ES6模块

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

UI框架

ElementUI,饿了么出

三、搭建vue.js项目

3.1 搭建vue.js项目

环境准备

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

安装 Vue CLI

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

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

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

输入 npm -v 查看npm版本号

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

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

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

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

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

npm install cnpm -g

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

3.2 构建前端项目

通用方法

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

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

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

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

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

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

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

访问 http://localhost:8080,查看网页 demo,大工告成!

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

区别

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

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

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多的项目,看人家怎么写的

主要文件详解

src——[项目核心文件]

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

index.html——[主页]

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

          vuedemo                

App.vue——[根组件]

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

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

            export default {  name: 'app'}            export default {  name: 'app'}

#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;}

【template】

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

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

【script】

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

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

【style】

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

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

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

    import './assets/css/public.css'

main.js——[入口文件]

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

前面我们说 App.vue 里的 和 index.html 里的 没有关系,那么这两个文件是怎么建立联系的呢?让我们来看入口文件 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: '',  components: { App }})

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

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

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

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

3.4 安装 Element-UI

Element 的官方地址为 http://element-cn.eleme.io/#/zh-CN

1.安装 Element

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

在这里插入图片描述

2.引入 Element

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

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

import ElementUI from ‘element-ui’

import ‘element-ui/lib/theme-chalk/index.css’

3.5 安装axios

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

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

3.6 安装Vuex

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

运行 npm install vuex –save

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

import Vue from ‘vue’

import Vuex from ‘vuex’

Vue.use(Vuex)

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

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

npm install –save vuex@3.6.2

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

npm install –save vue@3.0.2

npm install –save vuex@4.0.0

解决版本冲突

可能是npm版本问题报错

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

–legacy-peer-deps

3.7 安装VueRouter

npm install vue-router –save-dev

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

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

3.8 安装font-awesome

npm install font-awesome

四、前端拦截器

4.1 配置登陆拦截器

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

4.2 axios请求拦截器request

在vue项目中,我们通常使用axios与后台进行数据交互,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 = postRequestVue.prototype.putRequest = putRequestVue.prototype.getRequest = getRequestVue.prototype.deleteRequest = deleteRequest

五、登陆页面

5.1 样式设计

为了设计界面,我们需要关注的地方是 标签内的 html 和 标签内的 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。

/captcha返回信息

/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) ” />

系统登录

登录 export default { name: ‘Login’, components: {}, props: [], data() { return { // 验证码 captchaUrl:’/captcha” />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)

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

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

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

前端反向代理

vue.config.js

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

 proxyTable: {      '/': {        changeOrigin: true, //跨域        target: 'http://localhost:8081',        pathRewrite: {          // '^/api': ''        }      },       },

5.7 运行项目

六、首页页面

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

能够在每个页面显示

美观

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

,也就是子页面(组件)显示的地方。

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

6.1 菜单功能设计与实现

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

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

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

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

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

配置store/index.js

通过vuex进行路由状态管理

import Vue from 'vue'import Vuex from 'vuex'Vue.use(Vuex)// 导入 Vuexconst 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 Vuexexport 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('/" />6.3 样式设计 

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

布局使用了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代码

                    云办公                                                          {{ user.name }}                                    个人中心              设置              注销登录                                                                                                                            {{ item.name }}                                          {{ children.name }}                                                                                    首页            {{ this.$router.currentRoute.name }}                                欢迎来到云办公系统!                                                    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('此操作将注销登录, 是否继续" /> 

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

6.5 更新路由router/index.js

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

/home路由从首页获取

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消除边距

添加样式

              yeb-front              

七、基础信息设置

7.1 样式设计

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

Tabs 标签页

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

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

      用户管理    配置管理    角色管理    定时任务补偿    export default {    data() {      return {        activeName: 'second'      };    },    methods: {      handleClick(tab, event) {        console.log(tab, event);      }    }  };

7.2 组件化开发

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

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

组件目录

7.3 SysBasic.vue

                                          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: {}}

7.4 部门管理DepMana.vue组件

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

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

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

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

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

                                                {{ data.name }}                   showAddDep(data)">            添加部门                               deleteDep(data)">            删除部门                                                        
上级部门 {{ pname }}
部门名称
取 消 确 定 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 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的数据修改。

                        添加                                                                                                            编辑                        删除                                                批量删除                      职位名称                            取 消        确 定            export default {  name: "PosMana",  data() {    return {      pos: { // 查询添加职位数据        name: ''      },      positions: [],      dialogVisible: false,      updatePos: { // 更新职位数据        name: ''      },      multipleSelection: [] // 批量删除勾选的对象    }  },  mounted() {    // 调用获取后端接口所有职位数据方法    this.initPositions()  },  methods: {    // 批量删除请求    deleteMany() {      this.$confirm('此操作将永久删除[' + this.multipleSelection.length + ']条职位, 是否继续" />7.6 职称管理JobLevelMana.vue组件 

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

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

JobLevelMana.vue

                                              添加                                                                                                                          已启用            未启用                                                编辑                        删除                                          批量删除                        
职称名称
职称等级
是否启用
取 消 确 定 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 + ']条职称, 是否继续" />7.7 权限组PositionMana.vue组件

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

ROLE_

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

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

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

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

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

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

                    ROLE_                  添加角色                                                              可访问资源                                                                                                取消修改                确认修改                                                        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 + ']角色, 是否继续" /> {        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        }      })    }  }}.positionManaTool {  display: flex;  justify-content: flex-start;}.positionManaTool .el-input {  width: 300px;  margin-right: 6px;}.positionManaMain {  margin-top: 10px;  width: 700px;}

八、操作员管理

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

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

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

SysAdmin.vue

                            搜索                                                      {{ admin.name}}                                                                           用户名:{{ admin.name }}          手机号码:{{ admin.phone }}          电话号码:{{ admin.telephone }}          地址:{{ admin.address }}          用户状态:                                                                    用户角色:                          {{ role.nameZh }}                                                                                                                                                                                                                备注:{{ admin.remark }}                    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 九、员工资料 

一、展示所有员工

二、分页展示

三、员工搜索

四、员工添加

五、更新和删除

六、导入导出数据

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

npm install js-file-download

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

        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

                            <!-- 20、搜索 v-model="empName" 搜索             22、清空 clearable @clear="initEmps" -->                                    搜索                                                                                  {{                importDataBtnText              }}                                            导出数据                              添加员工                                <!-- 28-6 添加展开动画效果  包含整个搜索条件框  -->                                                        政治面貌:                                                                                                  民族:                                                                                                  职位:                                                                                                  职称:                                                                                                  聘用形式:                              劳动合同                劳务合同                                                                                    所属部门:                                                                                                                              {{ inputDepName }}                <!-- 23-25 回显数据 {{inputDepName}} -->                                                                入职日期:                                                                  取消                            搜索                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      {{ scope.row.contractTerm }}            年                                                            编辑            <!-- 查看高级资料 -->                        删除                                                                                                <!-- 23-6、<el-row                                                                                                                                                            男                  女                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  {{ inputDepName }}                  <!-- 23-25 回显数据 {{inputDepName}} -->                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            劳动合同                  劳务合同                                                                                                      未婚                  已婚                  离异                                                                                取 消                确 定            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、分页        }      });    }  }}/*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;}

十、工资账套管理

树形结构

SalSob.vue

                      添加工资账套                                                                                                                                                                                                                                                                                                                                                                                                      编辑            删除                                                                                                                                                                    {{ activeItemIndex === 10 " />{{ activeItemIndex === 10 ? '完成' : '下一步' }}            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        }      })    }  }}

十一、员工账套设置

SalSobCfg.vue

                                                                                                                                
基本工资 {{ scope.row.salary.basicSalary }}
交通补助 {{ scope.row.salary.trafficSalary }}
午餐补助 {{ scope.row.salary.lunchSalary }}
奖金 {{ scope.row.salary.bonus }}
养老金比率 {{ scope.row.salary.pensionPer }}
养老金基数 {{ scope.row.salary.pensionBase }}
医疗保险比率 {{ scope.row.salary.medicalPer }}
医疗保险基数 {{ scope.row.salary.medicalBase }}
公积金比率 {{ scope.row.salary.accumulationFundPer }}
公积金基数 {{ scope.row.salary.accumulationFundBase }}
{{ scope.row.salary.name }} 暂未设置 修改工资账套 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/" />十二、聊天功能

安装npm install --save stompjs

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

简介

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

下载(用chrom打开下载)

地址:

https://github.com/is-liyiwei/vue-Chat-demo

整合项目

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

在这里插入图片描述

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

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

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

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

components

card.vue

      

{{user.name}}

export default { name: 'card', data () { return { user:JSON.parse(window.sessionStorage.getItem("user")) } }}#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; }}

list.vue

      
  • <!-- 未读消息提示 小红点 -->

    {{ item.name }}

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) } }}#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; }}

message.vue

      
  • {{ entry.date | time }}

    {{ entry.content }}

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) } }}#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; } }}

usertext.vue

        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 = '';      }    }  }}#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;  }}chat/FriendChat.vue                                            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  }}#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;  }}

十三、个人中心

AdminInfo.vue

                    {{ admin.name }}                                                              电话号码:            {{ admin.telephone }}                    手机号码:            {{ admin.phone }}                    居住地址:            {{ admin.address }}                    用户标签:            {{ r.nameZh }}                                              修改信息                    修改密码                                        
用户昵称:
电话号码:
手机号码:
用户地址:
取 消 确 定 提交 重置 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) } }) } }}

十四、源代码

前端地址:https://github.com/OneDayInMarch/yeb-front

后端地址:GitHub - OneDayInMarch/yeb-back: 云办公后台系统