Cloud E office Springboot+vue - the full version of the front-end project (including source code)

1. Project introduction

Project background: Affected by the epidemic, many companies have switched from online to offline office. With the increase in the number of people working online, the advantages of online office gradually become prominent: by automating workflow, saving corporate office costs, realizing green office, and improving office efficiency.

Project introduction: This project implements an online office system to manage daily office affairs: daily process approval, news, notices, announcements, file information, finance, personnel, expenses, assets, administration, projects, mobile office, etc. Through the software, the office system can be managed according to the convenience, and the overall management and operation level can be improved.

Implementation method: This project is based on Vue+Spring Boot to build a front-end and back-end separation project. The front end is built using the open source framework Vue, which is very active in the community. Simply put, the core idea of ​​front-end and back-end separation  is that the front-end page calls the back-end restuful api for data interaction through ajax, while a single page application (single page web application, SPA) has only one page, and when the user interacts with the application A web application that dynamically updates the page from time to time.

1.1 Technical Architecture

1.2 Front-end technical architecture

This project adopts the development mode of front-end and back-end separation, and uses Spring Boot to build the back-end.

The technologies used in the front end are:

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

Project construction: Vue-cli; state management: Vuex; routing management: VueRouter; UI interface: ElementUI; communication framework: Axios;

Front-end syntax: ES6; packaging tool: Webpack; online chat: WebSocket; font: font-awesome; file upload and download: js-file-download; online chat open source project: vue-chat

The front-end modules are divided into: login, position management, title management, department management, operator calendar, employee management, salary account management, personal center, online chat

1.3 Cloud E Office (front end)

JSON data is communicated between the front and back ends through the RESTful API. Unlike JSP and the like, the backend does not involve the content of the page itself. When developing, the front-end uses the front-end server (Nginx), and the back-end uses the back-end server (Tomcat). When I develop the front-end content, I can forward the front-end request to the back-end through the front-end server (called reverse proxy ), so that you can observe the results in real time, and you don't need to know how the backend is implemented, but only need to know the functions provided by the interface.

Table of contents

1. Project introduction

1.1 Technical Architecture

1.2 Front-end technical architecture

1.3 Cloud E Office (front end)

2. Vue.js framework

2.1 Vue features

2.2 MVVM design pattern

2.3 vue-related components

3. Build the vue.js project

3.1 Build vue.js project

3.2 Build the front-end project

3.3 Vue project structure analysis

3.4 Install Element-UI

3.5 install axios

3.6 Install Vuex

3.7 Install VueRouter

3.8 Install font-awesome

4. Front-end interceptor

4.1 Configure login interceptor

4.2 axios request interceptor request

4.3 Axios response interceptor response

4.4 Package request

4.5 Code implementation src/utils/api.js

4.6 Main.js introduces encapsulation requests globally

5. Landing page

5.1 Style Design

5.2 Landing page function design

5.3 Login.vue login page

5.4 Configure page routing - router/index.js

5.5 Front-end routing and navigation guards

main.js

5.6 Solving front-end and back-end cross-domain issues

front-end reverse proxy

5.7 Running the project

6. Home page  

 6.1 Menu function design and implementation

Configure store/index.js

Introduce store in Main.js

6.2 Package menu request tool

The backend requests the menu interface to return information

update main.js

6.3 Style Design

6.4 Home.vue generation

6.5 Update route router/index.js

Ignore the hidden:true of router/index.js

6.6 index.html eliminate margins

7. Basic information setting

7.1 Style Design

Tabs tab page

7.2 Component-based development

7.3 SysBasic.vue

7.4 Department management DepMana.vue component

7.5 Position management PosMana.vue component

 PosMana.vue

7.6 Title management JobLevelMana.vue component

7.7 Permission group PositionMana.vue component

Eight, operator management

SysAdmin.vue

9. Employee information

 EmpBasic.vue

10. Salary account management

 SalSob.view

11. Staff account set setting

SalSobCfg.vue

12. Chat function

integration project

components

card.view

list.vue

message.view

usertext.vue

Thirteen, personal center

 AdminInfo.vue

Fourteen, source code


2. Vue.js framework

Web front-end development has been developed for many years from the beginning to the rise and now, and has accumulated a lot of development experience and development tools. Some of the experience of past developers has also been tested by technological development and environmental changes. The ideas, technologies and tools they created are also very worthy of reference and learning by our later developers, and even directly use them. Because no matter how development tools and development languages ​​develop, no matter how different they are, the problems they solve are similar and unified, which can be summarized as follows:

(1) Expand the language capabilities of the three programming languages ​​JavaScript, html and CSS;

(2) Solve the repetitive work in the development process;

(3) Modularize the project;

(4) Solve the problem of function reuse and change;

(5) Solve the problem of differences between development and product environments;

(6) Solve the problem of the release process.

In order to solve the above problems, the idea of ​​engineering came into being, and engineering is the best practice to avoid repeated wheel creation. Vue.js is a progressive framework for building user interfaces. It was developed by Chinese program developer You Yuxi in 2013. Because Vue.js' concise syntax design, lightweight and fast features are popular among developers in the technical community, it also promotes the promotion and popularity of Vue.js. With the use of related tools and support libraries, Vue.js can perfectly drive complex single-page applications and develop a large-scale web application. Vue.js has jumped to the top 30 in the authoritative JavaScript trend list and can continue to rank at the forefront of the list. It has obviously become a top JavaScript framework in the world. The ecology of Vue.js is not only reflected in the trend list, its supporting data management library vuex, routing management library Vue-router, packaging tools, developer debugging plug-ins and project scaffolding and other tools and libraries have also been gradually developed, and also have Very active technical community.

Vue.js is a lightweight MVVM front-end framework that can be used to build progressive user interfaces. When developers build front-end pages in Vue.js, they only need to care about the implementation of page logic. The biggest feature of Vue.js is that it is applied layer by layer from the bottom layer. It is not only easy to use but also compatible with a large number of third-party libraries.

2.1 Vue features

Virtual DOM

The characteristics of vue that distinguish it from traditional frameworks are virtual DOM. The browser's DOM operation will bring a lot of overhead, so the Virtual DOM is built in Vue through the diff algorithm, and the Virtual DOM is rebuilt every time the data is updated with the smallest change.

Responsive

The core of Vue.js is designed as a responsive data binding system, so it is very convenient to keep data in sync with the DOM. Manually manipulating the DOM with jQuery is often prone to writing imperative, repetitive, and error-prone code. And Vue.js embraces the concept of data-driven views, which means using special syntax in ordinary HTML templates to "bind" the DOM to the underlying data. Once this binding is created, the DOM is kept in sync with the data. Whenever the data is modified, the DOM will be updated accordingly. In this way, in the development of applications, almost all business logic only needs to directly modify the data, without having to perform a separate update operation on the DOM, so that the data and DOM updates will not be mixed together. This also makes the application code easier to write, understand and maintain.

Componentization

In large-scale application development, applications are often abstracted into multiple relatively independent modules for the purpose of reusability and maintainability of code blocks. However, only when reusability is considered will a certain module be made into a separate component. In fact, the view interface of the Web can also be divided into a component tree. Componentization is one of the most powerful features of Vue.js. Components can extend and encapsulate the label elements of the view page, and finally become reusable code. From a high-level understanding, a component can also be a custom element, and then through the compilation of the Vue.js compiler, some special functions can be added to this element. At the same time, components can also be native HTML elements, extended through the is attribute. Vue.js is somewhat similar to React.js, a front-end framework that also emphasizes the idea of ​​componentization, but it is lighter, simpler and more advanced than it.

Componentization usually means that Vue.js can write JavaScript code, hypertext markup language (hypertext markup language, HTML) code and cascading style sheets (cascading style sheets, CSS) code in the same file. In actual development, developers often encounter the situation that the functions of the page need to be used multiple times. At this time, they can build reusable components in the components directory. If other pages need to use this component, it can be introduced through the import method. Since the page is composed of multiple components, the coupling between components is low, which can greatly reduce repetitive code.

partial refresh

Vue is a single-page application. The main feature of a single-page application is the partial refresh of the webpage. The webpage application calls AJAX through controlling the routing, and the background only needs to provide an interface to realize it. Such an application has obvious advantages. First of all, it will be more user-friendly in terms of user experience and does not need to refresh the entire page, so the loading speed is fast and the experience is better.

2.2 MVVM design pattern

When the Java Web application system based on B/S architecture is being developed, the drawing and beautification of the front-end page is an important task of system development. Page drawing and interaction are generally based on the manipulation of document object model (document object model, DOM) element nodes and data, but direct manipulation of DOM nodes is extremely prone to errors. In recent years, with the development of front-end technology, various front-end frameworks have emerged. These frameworks are based on the MVVM (Model-View-ViewModel) design pattern, which brings a lot of convenience to the development and maintenance of front-end projects. The MVVM design pattern is derived from the traditional MVC design pattern, and its full name is Model-View-ViewModel. The Model layer is responsible for holding user data, the View layer is responsible for displaying visual elements and controls on the screen, and the ViewModel layer is responsible for converting the model into values ​​that can be displayed directly on the view.

2.3 vue-related components

Scaffolding vue-cli

Rapid development tool vue-cli . It can help developers develop quickly based on the vue.js framework. Vue-cli standardizes various tools to ensure that various construction tools can be smoothly connected based on intelligent default configurations, so that developers can focus more on writing front-end applications without spending a long time adjusting project configurations.

Routing Vue-router

Single-page web applications built with Vue.js need to be based on routing and components. Among them, the main function of routing is to set the access path and map the access path and view components. In a single-page web application, jumps and switches between paths are actually switches between corresponding components. Like React.js, Vue.js itself does not have a routing function. Therefore, when using the Vue.js framework, it needs to cooperate with a routing tool library, namely Vue-router. Vue-router can map different levels and nested routing relationships to corresponding nested components, and provides a fine-grained solution to control path jumps.

state management vuex

The view changes of Vue.js are based on the state of the component, so when building a large-scale web application, a large number of component states will be generated, and these states need to be managed. Because vue's "one-way data flow" cannot solve the problem of multiple components sharing state, so there is vuex. Vuex is a management framework that centrally stores and manages the state of all components of the application, and is specially designed for applications that use the Vue.js framework. It draws on the design concepts of Flux and Redux, the state management tools of React.js, and simplifies some concepts, so that it can better utilize the data response mechanism of Vue.js.

communication framework axios

communication framework. Because the boundary of vue is very clear, it is for processing DOM, so it does not have communication capabilities. At this time, it needs to use an additional communication framework to interact with the server; of course, you can also directly use the AJAX communication function provided by jQuery. The classic Ajax technology realizes the partial data refresh of the webpage, and Axios re-encapsulates Ajax, which has the following characteristics:

(1) Create an XMLHttpRequest from the browser

(2) Send http request from node.js

(3) Support Promise API

(4) Intercept requests and responses

(5) Conversion request and corresponding data

(6) Cancellation request

(7) Automatically convert JSON data

(8) The client supports preventing CSRF/XSRF

The Axios plug-in encapsulates the Ajax technology very well, and the writing method is concise and clear in project development, so it is not easy to make mistakes, and even if there are mistakes, it is easy to troubleshoot.

Packaging tool webpack

The main difference between front-end development and other development work is that the front-end is based on multi-language, multi-level coding and organization work, and secondly, the delivery of front-end products is based on the browser, and these resources are run to the browser through incremental loading. , How to organize these fragmented codes and resources in the development environment, and ensure their fast and elegant loading and updating in the browser, requires a modular system.

webpack is a modular tool for large-scale js applications, which automatically handles a series of js programming problems such as namespaces. When webpack processes a js application, it will build a complex dependency graph, which contains modules that the application depends on, and even contains many static resources, and then webpack will package these modules into one or more Large modules, referenced in the application.

ES6 modules

Vue is usually written in es6 and exported with export default, which can contain data, life cycle (mounted, etc.), methods (methods), etc. For specific syntax, please refer to the vue.js document. The ES6 standard adds a module system definition at the javascript language level. The design idea of ​​ES6 modules is to be as static as possible, so that the dependencies of the modules, as well as the input and output variables can be determined at compile time. Both CommonJS and AMD modules can only determine these things at runtime.

UI framework

ElementUI, launched by Ele.me

3. Build the vue.js project

3.1 Build vue.js project

Environmental preparation

Install Node.js (>=6.x, preferred 8.x) This project is version v14.18.0

Install Vue CLI

Because you need to use npm to install Vue CLI, and npm is integrated in Node.js, so the first step we need to install Node.js, visit the official website Node.js , you can download it from the home page.

After the download is complete, run the installation package and follow the next step.

Then enter node -v in cmd to check whether node is installed successfully.

Enter npm -v to view the npm version number

Enter npm -g install npm to update npm to the latest version.

After that, install the scaffolding with npm install -g vue-cli  . (This project uses version 2.9.6)

Note that the 2.x version of Vue CLI is installed in this way, and the latest version needs to be installed through npm install -g @vue/cli . The new version can use the graphical interface to initialize the project and add the content of project health monitoring, but the project dependencies created with the new version do not match this tutorial, and it is troublesome to toss.

Taobao mirror accelerator cnpm installed Node.js

In most cases, use npm, and use cnpm if you can’t install it

npm install cnpm -g

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

3.2 Build the front-end project

Common method

Build projects directly using the command line.

Then execute the command vue init webpack yeb, where webpack uses webpack as a template to refer to the generated project, and can also be replaced with parameters such as pwa and simple, which will not be described here.

There will be some prompts during the execution of the program. You can press Enter all the way according to the default settings, or you can modify it as needed. For example, if the following picture asks me if the project name is wj-vue, just press Enter to confirm.

Here you will also be asked whether to install vue-router, you must choose yes, that is, press Enter or press Y, vue-router is the key for us to build a single-page application.

And whether to use es-lint, select N.

Then wait for the project to build and it will be OK.

You can see that the project folder is generated in the workspace directory, you need to execute npm install in this folder, npm run build and then execute npm run dev

Visit http://localhost:8080 , view the webpage demo, and you're done!

Note: In the vue project, sometimes you need to execute npm run serve to start the project, and sometimes you need to use npm run dev. What is the difference?

the difference

By default, dev is the command supported by [email protected] by default ;

By default, serve is the command supported by [email protected] and above.

3.3 Vue project structure analysis

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

Detailed explanation of main documents

src - [project core file]

In the vue-cli project, the src folder must be mastered, because basically everything to be done is in this directory.

index.html - [Home Page]

index.html is the same as other html, but generally only defines an empty root node. The instance defined in main.js will be mounted under the root node, and the content will be filled by vue components, and the built file will be automatically injected , which means that other content we write will be displayed in this div. There is only one html file in the whole project, so this is a single-page application. When we open this application, there may be many pages on the surface, but in fact they are all in one div.

<!DOCTYPE html>

<html>

  <head>

    <meta charset="utf-8">

    <title>vuedemo</title>

  </head>

  <body>

      <!-- 定义的vue实例将挂载在#app节点下 -->

    <div id="app"></div>

  </body>

</html>

App.vue - [root component]

This file is called the "root component" because other components are included in this component. A .vue file is a custom file type, similar in structure to html, and a .vue file is a vue component.

A vue page usually consists of three parts: template (template), js (script), style (style)

<!-- template -->

<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 -->

<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>

【template】

The template can only contain one parent node, that is to say, there can only be one top-level div (for example, in the above code, the parent node is the div whose parent node is #app, which has no sibling nodes). There is also a <div id="app"> here, but it has nothing to do with the one in index.html. This id=app just corresponds to the following css.

<router-view></router-view> is a sub-routing view, and all subsequent routing pages are displayed here. To make a metaphor, <router-view> is similar to a slot. When jumping to a certain route, the page under the route is inserted in this slot for rendering and display.

【script】

The content in the <script> tag is the script of the component, that is, the js code. The export default is the syntax of ES6, which means to export this component as a whole, and then you can use import to import the component. The content in curly brackets is the relevant properties of this component.

Vue is usually written in es6 and exported with export default, which can contain data, life cycle (mounted, etc.), methods (methods), etc. For specific syntax, please refer to the vue.js document.

【style】

The style is wrapped by the style tag, which affects the whole world by default. If you want to define the scope to only work under this component, you need to add scoped to the tag.

If you want to import external css files, you first need to install the css-loader dependency package for the project, open cmd, enter the project directory, enter npm install css-loader, and press Enter.

After the installation is complete, you can import the required css files under the style tag, for example:

<style>

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

</style>

main.js - [entry file]

main.js mainly introduces the Vue framework, root components and routing settings, and defines the Vue instance. The following components: {App} is the imported root component App.vue. Plug-ins can also be introduced later, of course, the plug-ins must be installed first.

Earlier we said that <div id="app"> in App.vue has nothing to do with <div id="app"> in index.html, so how do these two files connect? Let's look at the code of the entry file 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 }

})

Several modules are imported at the top, among which the vue module is in node_modules, App is the component defined in App.vue, and router is the route defined in the router folder.

Vue.config.productionTip = false , the function is to prevent vue from generating production tips at startup.

In this js file, we create a Vue object (instance), and the el attribute provides a DOM element that already exists on the page as the mounting target of the Vue object, here through <div id="app in index.html The id="app" in "><div> and the "#app" here are mounted.

router means that the object contains Vue Router and uses the routes defined in the project. components indicates the Vue components contained in the object, and template uses a string template as the identifier of the Vue instance, which is similar to defining an html tag.

3.4 Install Element-UI

The official address of Element is http://element-cn.eleme.io/#/zh-CN

1. Install Element

According to the description of the official document, in the project folder, execute npm i element-ui -S

insert image description here

2. Introduce Element

Import is divided into two modes: full import and on-demand import. On-demand import can reduce the size of the project. Here we choose full import.

According to the documentation, we need to modify main.js as follows

import ElementUI from 'element-ui'

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

3.5 install axios

Go to the project folder and execute

npm install --save axios to install this module.

3.6 Install Vuex

Vuex, it is a state management solution specially developed for Vue, where we can define the variables and methods that need to be passed and used in each component. I haven’t used it before, so it’s a headache to pass values ​​from different components, and I have to write a lot of redundant code to call the values ​​​​of different components, so I recommend that you get familiar with this management method from the beginning.

run npm install vuex --save

After that, create a folder store in the src directory, and create a new index.js file in the directory, and introduce vue and vuex in the file, the code is as follows:

import View from 'view'

import Vuex from 'vuex'

Vue.use(Vuex)

Install vuex start error "export 'watch' was not found in 'vue'

If your vue version is 2.X, you can solve it by upgrading vuex to 3.XX

npm install --save [email protected]

If your vue version is 3.X, you can solve it by upgrading vuex to 4.XX

npm install --save [email protected]

npm install --save [email protected]

resolve version conflicts

It may be an npm version problem and an error is reported

Solution: add after the command

  --legacy-peer-deps

3.7 Install VueRouter

npm install vue-router --save-dev

Vue-router is the official routing plugin of Vue.js. It is deeply integrated with vue.js and is suitable for building single-page applications. Vue's single-page application is based on routing and components. Routing is used to set access paths and map paths and components.

Under the router folder, there is an index.js, which is the routing configuration file. Multiple routes can be set, such as '/index', '/list', etc. Of course, the component must be introduced first, and then the route should be set for the component.

3.8 Install font-awesome

npm install font-awesome

4. Front-end interceptor

4.1 Configure login interceptor

As the name implies , the interceptor is the interception of the request, the preprocessing work before or after the request interface. They are request interceptor and response interceptor respectively, execution order: request interceptor -> api request -> response interceptor. The role of the interceptor: a. Count the time required for the API to return data from the initiation of the request; b. Configure public request headers, load pop-up windows, etc.; c. Intercept the response status code and return 400 or 500 from the backend Status code, returns the corresponding error message.

4.2 axios request interceptor request

In the Vue project, we usually use axios to interact with the background data. Axios is a promise-based library that can run in the browser and node environments. The function of the request interceptor request: perform certain operations uniformly before the request is sent, and is often used to process tokens in the request header, etc. 

How to add a request interceptor

axios.interceptors.request.use(function (config) {

// 在发送请求之前做些什么

return config;

}, function (error) {

// 对请求错误做些什么

return Promise.reject(error);

})

4.3 Axios response interceptor response

The returned object response contains response.status: Http response code; response.data: the Json object returned by the backend, including response.data.code business logic response code, response.data.message: the response prompt message returned by the backend;

Add response interceptor method

axios.interceptors.response.use(function (response) {

// 对响应数据做点什么

return response;

}, function (error) {

// 对响应错误做点什么

return Promise.reject(error);

});

}

4.4 Package request

 In the project, we will not use axios directly, but will encapsulate it in one layer. Export the encapsulated request through export, such as defining a postRequest method to receive url and params, and then axios object. Perform the actual interface call operation in axios.

export const postRequest = (url, params) => {

    return axios({

        method: 'post',

        url: `${base}${url}`,

        data: params

    })

}

4.5 Code implementation 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

    })

}


Interceptors , as the name implies, intercept requests, which are request interceptors and response interceptors respectively. The order of execution is: request interceptor -> api request -> response interceptor. The role of the interceptor: a. Count the time required for the API to return data from the initiation of the request; b. Configure public request headers, load pop-up windows, etc.; c. Intercept the response status code and return 400 or 500 from the backend Status code, returns the corresponding error message.

4.6 Main.js introduces encapsulation requests globally

The method is introduced globally through main.js and then used through a plug-in. Use the form of this.putRequest(url, params) when calling

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. Landing page

5.1 Style Design

In order to design the interface, what we need to focus on is the html inside the <template> tag and the css inside the <style> tag. We usually use Form to make the login box, open the Element component document ( http://element-cn.eleme.io/#/zh-CN/component/ ), and find that it provides us with a wealth of Form components, we You can click "Show Code" to copy the part we need.

However, it seems that there is no form that is particularly suitable for our application scenario, or these are relatively complicated, and we only need a small part of them. Pull down the page, and you can see the documentation about the properties, events, methods, etc. of this component. According to this documentation, we can build the required form by ourselves.

5.2 Landing page function design

5.3 Login.vue login page

The verification code returns an image through the backend. Forms bind rules through rules, add attributes to elements through props, and write rules in rules. Validation method: this.$refs.loginForm.validate.

/captcha returns information

 /login login return message

A token will be returned after successful login. This token will be used as a credential for data interaction between the front and back ends. In order to ensure system security, the backend will periodically update the token, and the user needs to log in again after the expiration of the token. The front end stores the acquired token in sessionStorage as the key for calling the interface later, and this token needs to be brought with it when making get or post requests through Axios.

Determine whether the token exists in the request interceptor, and then verify the token for each request. If it exists, the request carries the token and puts it in the Authorization parameter; the backend verifies the token.

After the front-end login is successful, use this.$router.replace('/home') to jump to the home page. After the replace method is replaced, clicking the browser back button will not jump to the login page. If the login fails, the backend returns the reason for the failure.

When the user is not logged in, if the user does not visit the login page with http://localhost:8080/#/ , but visits a route that can only be accessed after login, such as http://localhost:8080/#/sys /basic . It needs to be discussed according to the situation: 1. The user may enter the home page address or a wrong address, and let him jump to the home page after successful login; 2. Otherwise, he will jump to the address he entered successfully.

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>

 In SessionStorage.setItem(), in order to enable axios to obtain token authentication when making the next request, after logging in, get the token and put it in sessionStrorage         

    // Store user token to sessionStorage

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

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

5.4 Configure page routing - 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 Front-end routing and navigation guards

The development of the login page seems to be relatively complete, but in fact it is not finished, because this login page is actually useless, and others can bypass the login page by directly entering the URL of the homepage. In order for this to work, we also need to develop an interceptor . Use the hook function to determine whether to intercept the function and the function that will be called at certain times. Here we use router.beforeEach(), which means calling before visiting each route. to is the route to go; from is the route from where; next() releases.

Get the user's token through sessionStorage.getItem('user'). If the token does not exist, you need to log in.

When judging whether it is an if (to.path == '/') landing page, if yes, let it go, otherwise log in according to the route specified by the user;

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 Solving front-end and back-end cross-domain issues

The front-end port defaults to 8080. Assuming the back-end port is 8081, how does 8080 access the data of 8081? We implement automatic port forwarding through Node.js. The browser's same-origin policy: two pages must have the same protocol (protocol) host (host) port number (port). The same-origin policy is a security mechanism of the browser, which means that the browser will prevent DOM operations on non-same-origin pages and XMLHttpRequest objects from initiating http requests to non-same-origin servers. When requesting an interface, Access-Control-Allow-Origin, etc. appear, indicating that the request is cross-domain. The way to solve cross-domain in vue: configure the vue.config.js file, if not, create a new one by yourself.

principle:

1. Send the domain name to the local server (localhost:8080)

2. Then the local server requests the real server

3. Because the request is sent from the server, there is no cross-domain problem.

In vue it is done automatically by node.js

front-end reverse proxy

view.config.js

Modify the proxyTable request address to proxy to the backend address 8081 after node.js

 proxyTable: {

      '/': {

        changeOrigin: true, //跨域

        target: 'http://localhost:8081',

        pathRewrite: {

          // '^/api': ''

        }

      },

   

    },

5.7 Running the project

6. Home page  

Our project, while essentially a single-page application, has multiple functional pages on the surface. To make it easier for users to switch between these pages, we need to add a navigation bar. The requirements for this navbar are simple:

able to display on every page

beautiful

In order to achieve the first requirement, we need to put the navigation bar in the parent page of other pages (for Vue, it is the parent component). As we said before, App.vue is the parent component of all components, but put the navigation bar in Going in is not appropriate because we shouldn't have a navigation bar in our login page. In order to solve this problem, we directly create a new component in the views directory and name it Home.vue. Like App.vue, write a

<router-view/>, which is where the subpages (components) are displayed.

Home.vue realizes the acquisition and display of the menu on the left side of the homepage as a whole, and the setting of the personal center in the upper right corner. Get the current menu information and login information of the current user from store.state.

 6.1 Menu function design and implementation

The required file directory is as follows: views/emp basic information Create a new EmpBasic.vue EmpAdv.vue

views/per employee profile new PerEmp.vu PerEc.vue PerTrain.vue PerSalary.vue PerMv.vue

views/sal Engineer SalSob.vue SalSobcfg.vue SalTable.vue SalMonth.vue SalSearch.vue

views/sta comprehensive information statistics added StaAll.vue StaScore.vue StaPers.vue StaRecord.vue

views/sys System Management New SysBasic.vue SysConfig.vue SysLog.vue SysAdmin.vue SysData.vue SysInit.vue

Configure store/index.js

Routing state management through vuex

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;

Introduce store in Main.js

import store from './store'

new Vue({

    router,

    store,

    render: h => h(App)

}).$mount('#app')

6.2 Package menu request tool

The backend requests the menu interface to return information

The menu we designed is the routing information loaded according to user information, that is, different users may have different menu permissions. The menu information returned by the interface is as follows. The submenu is represented by children, and when the parentId in the submenu is equal to the id of the parent menu, it indicates a certain parent-child menu relationship. The following relationship indicates that there is a hierarchical menu "employee profile/basic profile".

If store.state.routes has data, initialize the routing menu. Obtain routing data from the backend through the getRequest('/system/config/menu') method, and split it according to the hierarchical relationship.

How to find the corresponding code path according to the component field in the interface?

Find by classifying the component fields in the interface object, for example, the component starts with Home, and the source code is in src/views/Home.vue.

                if (component.startsWith('Home')) {

                    require(['@/views/' + component + '.vue'], resolve);

                }

The initMenu method stores routing data in the store. If there is data in the store, it does not need to be initialized; otherwise, it is initialized.

When is it called? Every page needs to call the initialization menu method. Put it in the route interceptor and execute it every time you access the route.

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

}

update main.js

Get current user login information

Save the current user information in the user of sessionStorage, and obtain the user's login information each time the route is switched.

// 使用 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 Style Design

The front-end page after login is decomposed into the upper navigation bar, the left menu bar and the main functional area in the middle. For switching between different pages, only the content of the middle functional area needs to be changed, which improves code reusability. First, customize the components of each area of ​​the page and save each component file in the Views folder. Each .vue file is a separate component. The components specified in the route are imported through the import statement <router-view></router-view> Rendered on the page.

The layout uses element-ui's container layout container: el-container outer container; el-header top bar container; el-aside sidebar container; el-menu navigation area; el-main main area container; el-footer bottom bar container

The style design is as follows:

Add the router attribute in the el-menu navigation to realize the dynamic rendering of the menu routing; the home page navigation menu uses the NavMenu navigation menu control of element-ui. Use the attribute unique-opened: to ensure that only one menu is expanded each time the menu is clicked. Use the router attribute to use index as the path for routing jumps when activating navigation.

Bind the command in el-dropdown-item with the event callback method triggered by clicking the menu item @command of el-dropdown to realize logout and login and enter the personal center function.

elemet's MessageBox bullet box realizes the logout and login prompt box. Clear the menu information in vuex after logging out.

Use the el-breadcrumb breadcrumb control to display the path of the current page and quickly return to any previous page. For non-home page v-if="this.$router.currentRoute.path!=='/home'" display level: first/current page.

For home page v-if="this.$router.currentRoute.path==='/home'", display welcome font.

6.4 Home.vue generation

<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>

Ignore the hidden attribute

            <!-- 2. Cycle the entire routing component, do not display the routing component with hidden: true -->

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

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

6.5 Update route router/index.js

Ignore the hidden:true of router/index.js

The / home route is fetched from the home page

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 eliminate margins

add style

<!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. Basic information setting

7.1 Style Design

System management/basic information setting design the following modules: department management, position management, title management, reward and punishment rules, authority group

Tabs tab page

Use the Tabs tab page of element to complete the switching of different business functions; separate data collections that are related but belong to different categories.

The abs component provides the tab function, the first tab page is selected by default, and you can also specify the currently selected tab page through the value attribute.

<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 Component-based development

In the process of our development, we will encounter many code blocks that can be reused, and Vue provides such a packaging method, which is Vue. component . Utilize component-based development to introduce component methods such as department management, position management, professional title management, reward and punishment rules, and authority groups. Components are also .vue files. Component import method import component name from "component path", example:

import DepMana from "@/components/sys/basic/DepMana"; cannot be used directly after importing, the components need to be registered in the components object. After that is the application of the component: <DepMana/> or <DepMana></DepMana/>

Component Catalog

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 Department management DepMana.vue component

For ease of use, load all departments at once.

Style design: flex layout, space-between: align both ends, align the width with the parent element.

The method of filtering nodes in the search box, pass in two parameters through the filterNode method, value-the value entered by the search, and the data bound to the data tree label. When the value is empty, all tag values ​​are displayed. Otherwise, it is judged whether the input value can be found in data.name (the value displayed on the tree tag). Here, the js search string method indexOf is used to return the subscript of the search element. Can find the return subscript (>=0)

 expand-on-click-node only expands when the mouse clicks the expand button, which is convenient for adding and deleting functions.

When the button is clicked, the data of the current node is obtained, that is, the "superior department" id (parentId) of the sub-department to be added

<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 Position management PosMana.vue component

Element's table control, the first column is a multi-selection box, and its type="selection". Form binding multi-selection event @selection-change="handleSelectionChange"

 Modify the job title through the el-dialog edit button. Binding pop-up box display method: visible.sync="dialogVisible", dialogVisible=false when canceling, dialogVisible=true when clicking "Edit".

 batch deletion. Get multiple selection data through multipleSelection array, when multipleSelection is empty, batch delete button is disabled. Multiple selection information is displayed in the prompt box through multipleSelection

Interface data return information

 PosMana.vue

The keyboard event @keydown.enter.native="addPosition" automatically invokes the "add" button binding method after input.

After editing job information: refresh the list data and close the pop-up box.

Use the data copy Object.assign(this.updatePos, data) to assign the data in data to updatePos, so as to avoid the data modification of data by updatePos caused by shallow copy.

<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 Title management JobLevelMana.vue component

Title management realizes the functions of adding, updating, single deletion and batch deletion of professional titles.

Whether to enable the button in a single update using the switch control of element

 JobLevelMana.view

<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 Permission group PositionMana.vue component

Style design: el-collapse folding panel-"el-card card-"el-tree tree control is used sequentially from outside to inside

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

Use the element folding panel to display the character, using the accordion mode accordion of the folding panel, and only one panel can be expanded at a time.

The backend gets all user role interfaces

Bind to the nameZh role field returned by the interface and display it on the foldable panel

The menu access rights of each role in the folding panel, using the tree control to set the role rights. el-tree adds the show-checkbox attribute to display the optional box.

The role menu list selected by default obtains the menu selectedMenus returned by the backend interface according to the role id. Binding via :default-checked-keys.

Use getCheckedKeys to get an array of selected node keys. let selectedKeys = tree.getCheckedKeys(true) // Get the selected node array, true only returns the keys of the selected leaf nodes, such as three-level classification

<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>

Eight, operator management

Operator information can be displayed individually by searching the operator's name. When displaying all operators, the currently logged-in operator will not be displayed.

The operator involves permissions: which roles the operator has, and which menu permissions the operator has according to the role.

Get operator management backend information return

 

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>

9. Employee information

1. Show all employees

2. Paging display

3. Employee search

4. Add employees

5. Update and delete

6. Import and export data

Axios itself does not provide a download function, you need to install js-file-download

npm install js-file-download

The output is in the form of a stream, and the format of the stream is a binary array. Similar to the interface request of axios, js-file-download also needs to encapsulate the request interceptor and response interceptor, because js-file-download does not share the interceptor function encapsulated by axios. The request interceptor needs to reset the setting of the request header Authorization. The response interceptor of axios is to judge the response code, and js-file-download needs to judge whether the returned is a json string. By judging the content-type in the returned header, if the content-type is in the application/json format, it is a normal json return, and the binary code needs to be converted to a normal string form. Non-json strings are returned in the form of streams, and fileName and contentType need to be obtained. In order to prevent possible garbled characters due to the fact that the file name is in Chinese, it is necessary to convert the format of the 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>

10. Salary account management

tree structure

 SalSob.view

<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>

11. Staff account set setting

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>

12. Chat function

Install npm install --save stompjs

Integrate the open source project on GitHub with your own project to realize the instant chat function

Introduction

A simple chat example based on Vue + Webpack, the chat history is saved in localStorge. Simple demonstration of Vue's component, filter, directive, computed and event communication between components. There is currently a bug in the original project: opening the project, closing the browser and opening it again will report an error. Here we use the project reconstructed on the basis of this project to integrate with our project.

Download (open the download with chrom)

address:

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

integration project

Add the file where the downloaded project is located to this project.

insert image description here

assets: It is the picture required by the web page, this is not needed, because the backend provides the picture, and the changed code is directly obtained from the backend.

components: Create several files in this directory in your own directory and copy them over.

vuex: it is stroy, add the code in it to the story/index.js of this project

main.js: It is an ordinary entry, and does not need to be added to the project for summary.

components

card.view

<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.view

<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>

Thirteen, personal center

 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>

Fourteen, source code

Front-end address: https://github.com/OneDayInMarch/yeb-front

Backend address: GitHub - OneDayInMarch/yeb-back: Cloud office background system

Guess you like

Origin blog.csdn.net/qq_36384657/article/details/126275840