Vue详解及综合案例

2023-11-15

一、Vue简介

1.1 简介

Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式的js框架,发布于 2014 年 2 月。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库(如:vue-routervue-resourcevuex)或既有项目整合。

1.2 MVVM 模式的实现者——双向数据绑定模式

  • Model:模型层,在这里表示 JavaScript 对象

  • View:视图层,在这里表示 DOM(HTML 操作的元素)

  • ViewModel:连接视图和数据的中间件,Vue.js 就是 MVVM 中的 ViewModel 层的实现者 

这里 核心就是 ViewModel 里面有DOM监听以及数据绑定,View是页面数据展示 Model也就是前面data里定义的,通过Vue来实现各种快捷功能,我们用普通js写的话 得写一大串Js代码;

mvvm设计模式 这里的

第一个m是 model 也就是vm的data属性

第二个v是 view 视图 网页模版

最后vm就是中间vue的 viewmodel 代码体现就是vm对象或者vm实例;

1.3 其它 MVVM 实现者

  • AngularJS

    简单介绍一下,AngularJS诞生于2009年,由Misko Hevery 等人创建,后为Google所收购。是一款优秀的前端JS框架,已经被用于Google的多款产品当中。AngularJS有着诸多特性,最为核心的是:MVVM、模块化、自动化双向数据绑定、语义化标签、依赖注入等等。

  • ReactJS

    React引入了虚拟DOM(Virtual DOM)的机制:在浏览器端用Javascript实现了一套DOM API。基于React进行开发时所有的DOM构造都是通过虚拟DOM进行,每当数据变化时,React都会重新构建整个DOM树,然后React将当前整个DOM树和上一次的DOM树进行对比,得到DOM结构的区别,然后仅仅将需要变化的部分进行实际的浏览器DOM更新。

  • 微信小程序

    微信小程序的视图层和数据层就是通过MVVM进行绑定的。

1.4 为什么要使用 Vue.js

  • 轻量级,体积小是一个重要指标。Vue.js 压缩后有只有 20多kb (Angular 压缩后 56kb+,React 压缩后 44kb+)

  • 移动优先。更适合移动端,比如移动端的 Touch 事件

  • 易上手,学习曲线平稳,文档齐全

  • 吸取了 Angular(模块化)和 React(虚拟 DOM)的长处,并拥有自己独特的功能,如:计算属性

  • 开源,社区活跃度高

1.5 Vue.js 的两大核心要素

1.5.1 数据驱动

当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是为什么 Vue 不支持 IE8 以及更低版本浏览器。

这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。这里需要注意的问题是浏览器控制台在打印数据对象时 getter/setter 的格式化并不同,所以你可能需要安装 vue-devtools 来获取更加友好的检查接口。

每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。

1.5.2 组件化

  • 页面上每个独立的可交互的区域视为一个组件

  • 每个组件对应一个工程目录,组件所需的各种资源在这个目录下就近维护

  • 页面不过是组件的容器,组件可以嵌套自由组合(复用)形成完整的页面

二、Vue的初体验

2.1在页面引入vue的js文件即可。

注意:cdn是一种加速策略,能够快速的提供js文件

 <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>

2.2 在页面中绑定vue元素

创建一个div,id是app

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

2.3 创建vue对象,设计对象的内容

其中该vue对象,绑定了页面中id是app的那个div

<script>
        new Vue({
            el:"#app",
            data:{
                title:"hello vue!",
                  args1:"hi!",
                 age:18,
                flag:true
            }
        });
</script>
 #  el: element的简称,也就是Vue实例挂载的元素节点,值可以是 CSS 选择符,或实际 HTML 元素,或返回 HTML 元素的函数。
 #  data: 用于提供数据的对象,里面存放键值对数据。

说明:

这里有几点重要说明:

new Vue 我们创建了Vue对象;

el 指定了绑定DOM,接管DOM操作;

data:用于提供数据的对象,里面存放键值对数据

v-model 重点 可以实现数据双向绑定,改变了这里的值,其他地方也根据改变;

{{xxxxx}} 显示数据;

2.4 在页面的元素中使用插值表达式来使用vue对象中的内容

<div id="app">
    {{ title }}
</div>

三、 插值表达式

插值表达式的作用是在View中获得Model中的内容

Model中的内容如下:

new Vue({
        el:"#app",
        data:{
            title:"hello world!"
        },
        methods:{
            sayHello:function(){
                return "hello vue";
            }
        }
    });

3.1 简单使用插值表达式获取数据

<div id="app">
    {{title}}
</div>

此时,页面上将会显示"Hello world!"

3.2 在插值表达式中获取数组中的内容

<div id="app">
    {{[1,2,3,4][2]}}
</div>

此时,页面上会显示“3”,也就是数组中的第三个元素被获取。

3.3 使用插值表达式获取对象中的属性

<div id="app">
    {{ {"name":"xiaoyu","age":20}.age }}
</div>

此时,页面上会显示“20”,也就是对象中age属性的值。

3.4 使用插值表达式调用Vue中的方法

<div id="app">
        {{ sayHello()}}
</div>

此时,页面上会显示“hello vue”,也就是调用了vue对象中的sayHello方法,并展示了方法的返回值。

3.5Vue对象总结 

Vue.js通过加载js,实现对页面的快速渲染。vue封装的js该如何使用? 就必须了解MVVM双向数据绑定模式。Vue将视图层和数据层分离,通过MVVM建立视图层和数据层的连接。其中,插值表达式是一种连接方式,可以通过插值表达式以多种方式,快速的从数据层获取数据并展示在视图层上。数据层Vue对象,也是由很多部分组成,比如之前介绍的el,data,methods等,以及之后要介绍的mount,computed等。

四、Vue的分支 v-if

4.1 v-if

Vue中的分支语句v-if非常好理解,逻辑跟Java中的if-else相同。v-if语句块包含以下内容:

  • v-if

  • v-else

  • v-else-if

接下来以一个简单例子即可理解:

<html>
    <head>
        <meta charset="utf-8">
        <title></title>
    </head>
    <body>
        <div id="app">
            <p v-if="flag">
                今天天气很舒服!
            </p>
            <p v-else-if="rich">
                今天天气很燥热!晚上要去放松一下!
            </p>
            <p v-else="rich">
                晚上只能自嗨!
            </p>
        </div>
        
    </body>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
    <script>
        new Vue({
            el:'#app',
            data:{
                flag:false,
                rich:false
            },
            methods:{
            }
        });
    </script>
</html>

从这个例子可以看出,vue对象中的data提供了分支的条件。根据条件,如果是true,则v-if的内容就会显示,反之不显示。

4.2 v-show

v-if和v-show之间有着看似相同的效果,但优化上却有区别。先看下面这个例子:

<html>
    <head>
        <meta charset="utf-8">
        <title></title>
    </head>
    <body>
            <p v-show="rich">
                有钱!
            </p>
            <p v-if="rich">
                有钱!
            </p>
            <button type="button" @click="rich=!rich">今晚彩票开奖</button>
        </div>
        
    </body>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
    <script>
        new Vue({
            el:'#app',
            data:{
                flag:false,
                rich:false
            },
            methods:{
                
            }
        });     
    </script>
</html>

通过点击“今晚彩票开奖”按钮,能切换rich的值,此时发现,v-if和v-show的显示状态都会来回切换。看起来是一样的,但通过查看控制台代码发现,v-show实际会将p标签的css样式的display属性设为none来达到隐藏的效果。 

而v-if是直接在页面上添加和删除p标签来达到效果,因此v-show在反复切换的应用场景下,效率比v-if更高。

五、Vue的循环 v-for

Vue中的循环关键字并没有Java的那么多,只有v-for,但用法上有多种。接下来我们来逐一介绍。

5.1 普通的for循环

我们需要定义数据源,然后通过v-for来遍历数据源,再使用差值表达式输出数据。

<body>
<div id="app">
    <ul>
        <li v-for="item in items">{{item.date}}</li>
    </ul>
</div>
</body>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
<script>
    new Vue({
        el:'#app',
        data:{
            items:[1,2,3,4,5,6]
        }
    });
</script>

在这个例子中,数据源提供了一个数组。视图层通过v-for来循环输出多个li标签,非常简单。

5.2 带着索引的for

<body>
<div id="app">
    <ul>
        <li v-for=" (a,i)  in  args" :key='i'>{{i}}{{a}}</li>
    </ul>
</div>
</body>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.js"></script>
<script>
    new Vue({
        el:'#app',
        data:{
            args:[1,2,3,4,5,6]
        }
    });
</script>

此时的i就是每次循环的循环变量 ,从0开始一直到元素个数-1

5.3 遍历一个对象中的信息: value、name、index

<body>
<div id="app">
    <ul>
        <li v-for="(value,name,index) in student">{{name}}--{{value}}--{{index}}</li>
    </ul>
</div>
</body>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.js"></script>
<script>
    new Vue({
        el:'#app',
        data:{
            student:{
                username:'小鱼',
                age:20,
                girl:'如花'
            }
        }
    });
</script>

value、name、index 这几个字符可以自己定义,分别表示每次循环内容的值、键、序号。

  • value: 循环中每条数据的值 小鱼、20、如花

  • name: 循环中每天数据的键 username、age、girl

  • index: 循环的序号,从0开始

页面效果如下:

5.4 遍历对象数组:嵌套的for循环

<body>
<div id="app">
 <table style="border: 1px solid black">
            <tr>
                <th>姓名</th>
                <th>年龄</th>
                <th>联系方式</th>
            </tr>
            <tr v-for="(stu,index) in students" v-bind:key="index">
                <td>{{stu.name}}</td>
                <td>{{stu.age}}</td>
                <td>{{stu.phone}}</td>
            </tr>
        </table>
</div>
</body>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script></script>
<script>
    new Vue({
        el:'#app',
        data:{
             students:[
                {
                    name:'zs',
                    age:18,
                    phone:'111'
                },
                {
                    name:'ls',
                    age:18,
                    phone:'222'
                },{
                    name:'ww',
                    age:18,
                    phone:'333'
                }
            ]
        }
    });
</script>

可以清楚的看到,此时数据源是一个student数组,通过两层v-for循环,外层遍历数组中的每个student对象,内层v-for遍历每个对象的v、k、i。

六、Vue的属性绑定

Vue提供了多个关键字,能快速的将数据对象中的值绑定在视图层中。

6.1 v-model

通过v-model将标签的value值与vue对象中的data属性值进行绑定。

<html>
    <head>
        <meta charset="utf-8" />
        <title></title>
    </head>
    <body>
        <div id="app">
            <input type="text" v-model="title">
            {{title}}
        </div>
    </body>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
        new Vue({
            el:'#app',
            data:{
                title:"hello vue"
            }
            
        })
    </script>
</html>

此时input标签中加入了“v-model='title'”,表示input的value值与vue对象中的title属性绑定,当在input输入框中输入内容会实时修改title的值。于是{{title}}插值表达式能实时输出input输入框内的值。

页面效果如下:

 6.2 v-bind

我们知道插值表达式是不能写在html的标签的属性内的,那如果一定要用vue中的属性作为html标签的属性的内容,就可以通过v-bind进行属性绑定。

<html>
    <head>
        <meta charset="utf-8" />
        <title></title>
    </head>
    <body>
        <div id="app">
            <a v-bind:href="link"></a>
        </div>
    </body>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
        new Vue({
            el:'#app',
            data:{
                link:'http://www.baidu.com'
            }
        })
    </script>
</html>

这样,a标签内的href属性就可以使用vue对象中的属性值。

注意: v-bind也可以简写,使用冒号“:”来代替。

<a v-bind:href='link'></a>  等价于  <a :href='link'>

七、Vue的事件绑定 

关于事件,要把握好三个步骤:设参、传参和接参。

<html>
    <head>
        <meta charset="utf-8" />
        <title></title>
    </head>
    <body>
        <div id="app">
            sum={{sum}}<br/>
            {{sum>10?'总数大于10':'总数不大于10'}}<br/>
            <button type="button" @click="increase(2)">增加</button>
        </div>
    </body>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script type="text/javascript">
        new Vue({
            el:'#app',
            data:{
                sum:0
            },
            methods:{
                increase:function(s){
                    this.sum+=s
                }
            }
        })
    </script>
</html>

从这里例子中:

设参:

<button type="button" @click="increase(2)">增加</button>

传参: 

increase:function(s)

 接参:

this.sum+=s

注意:increase:function(s){ }函数可以写成increase(s){ }

接下来我们来看一下VUE中如何进行事件绑定。

7.1 v-on

通过配合具体的事件名,来绑定vue中定义的函数

<html>
    <head>
        <meta charset="utf-8" />
        <title></title>
    </head>
    <body>
        <div id="app">
            <input type="text" v-on:click="changeMajor"  />
        </div>
    </body>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script type="text/javascript">
        new Vue({
            el:'#app',
            data:{
                major:'java'
            },
            methods:{
                sayHi(){
                    alert("HELLO VUE!");
                },
                changeMajor(){
                    console.log("change Title")
                }
            }
    </script>
</html>

此时,该按钮,在点击时将会调用Vue对象中定义的changeMajor方法。

注意: v-on也可以简写,使用"@"替代。

<input type="text" @click="changeMajor"  />

7.2 事件修饰符

可以使用Vue中定义好的事件修饰符,快速达到效果。查看以下例子:

<html>
    <head>
        <meta charset="utf-8" />
        <title></title>
    </head>
    <body>
        <div id="app">
            <p @mousemove="mm">
                x:{{x}}
                y:{{y}}
                <span @mousemove.stop>鼠标移动到此即停止</span>
            </p>
            <div @click = "doDivClick" style="width: 100px;height: 100px;border: 1px solid black">
            <a @click.stop="doThis" href="https://www.baidu.com">
                百度
            </a>
        </div>
        </div>
    </body>
    <script   <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
        new Vue({
            el:'#app',
            data:{
                x:0,
                y:0
            },
            methods:{
                mm(event){
                    this.x = event.clientX;
                    this.y = event.clientY;
                },
                stopm(event){
                    event.stopPropagation();
                },
                doThis(){
                alert("执行到了a的click");
            },
                doDivClick(){
                alert("执行到了div的click");
                }
            }
        })
    </script>
</html>

当鼠标经过P标签区域内时,区域内就会显示X和Y轴的坐标,如果经过P标签内的Span标签内时,此时会调用事件属性mousemove.stop预定的效果,鼠标移动的效果将会被取消,X和Y不再显示信息。

7.3计算属性:computed

7.3.1 什么是计算属性

计算属性的重点突出在 属性 两个字上(属性是名词),首先它是个 属性 其次这个属性有 计算 的能力(计算是动词),这里的 计算 就是个函数;简单点说,它就是一个能够将计算结果缓存起来的属性(将行为转化成了静态的属性),仅此而已;

7.3.2 计算属性与方法的区别

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>布局篇 计算属性</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.js"></script>
</head>
<body>
​
<div id="vue">
    <p>调用当前时间的方法:{{currentTime1()}}</p>
    <p>当前时间的计算属性:{{currentTime2}}</p>
</div>
​
<script type="text/javascript">
    var vm = new Vue({
        el: '#vue',
        data: {
            message: 'Hello Vue'
        },
        methods: {
            currentTime1: function () {
                return Date.now();
            }
        },
        computed: {
            currentTime2: function () {
                this.message;
                return Date.now();
            }
        }
    });
</script>
</body>
</html>

说明

  • methods:定义方法,调用方法使用 currentTime1(),需要带括号

  • computed:定义计算属性,调用属性使用 currentTime2,不需要带括号;this.message 是为了能够让 currentTime2 观察到数据变化而变化

注意:methods 和 computed 里不能重名

7.3.3 测试效果 

仔细看图中说明,观察其中的差异

7.3.4 结论

调用方法时,每次都需要进行计算,既然有计算过程则必定产生系统开销,那如果这个结果是不经常变化的呢?此时就可以考虑将这个结果缓存起来,采用计算属性可以很方便的做到这一点;计算属性的主要特性就是为了将不经常变化的计算结果进行缓存,以节约我们的系统开销

八、Vue的组件化

8.1 什么是“组件化”

Vue的组件化设计思想借鉴了Java的面向对象思想。Java认为万物皆对象,在Vue中,万物皆组件。

也就是说,在实际的vue项目中,以及使用了Vue框架的项目中,Vue的对象都会以组件的形式出现,能被反复使用。

要想实现组件化,需要在页面中注册组件:关于注册的方式有两种,分别是全局注册和本地注册。

8.1.1 组件的全局注册

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>vue组件的全局注册</title>
</head>
<body>
    <div id="app">
        <model1></model1>
        <model1></model1>
        <model1></model1>
    </div>
        <hr/>
    <div id="app1">
        <model1></model1>
        <model1></model1>
        <model1></model1>
    </div>
</body>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
<script>
    //通过Vue.component实现组件的全局注册,全局注册后的组件可以被重复使用。
    Vue.component("model1",{
​
        template:"<div><h1>{{title}}</h1><button type='button' @click='btnfn'>点我</button></div>",
        data:function(){
            return {
                title:"hello vue"
            }
        },
        methods:{
            btnfn:function(){
                alert("hello !!!");
            }
        }
    });
    new Vue({
        el:'#app'
    })
    new Vue({
        el:'#app1'
    })
</script>
​
</html>

8.1.2 组件的本地注册

vue的全局注册,也就意味着在页面的任意一个被vue绑定过的div中,都可以使用全局注册了的vue组件。

但是,如果是对vue组件进行本地注册,那么在其他被vue绑定的div中,不能使用该组件。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>vue组件的本地(局部)注册</title>
</head>
<body>
    <div id="app">
        <model11></model11>
    </div>
    <hr/>
    <!--在这里使用组件model11会报错-->
    <div id="app1">
        <model11></model11>
    </div>
</body>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
<script>
    new Vue({
        el:'#app',
        components:{
            "model11":{
​
                template:"<div><h1>{{title}}</h1><button type='button' @click='btnfn'>点我</button></div>",
                data:function(){
                    return {
                        title:"hello vue"
                    }
                },
                methods:{
                    btnfn:function(){
                        alert("hello !!!");
                    }
                }
            }
        }
    })
    new Vue({
        el:'#app1'
    })
</script>

8.1.3 小结

这是一个完整的Vue组件。该组件包含了三个部分:template(html视图层内容)、script(Model层)、style(CSS样式)。这样封装好的组件可以被复用,也可以作为其他组件的组成部分而被封装——Java的面向对象再次体现。

  • 特点1: template标签内,必须有且只能有一个根标签。

  • 特点2: componet中注册的组件中的data,必须是已函数的形式。

    如下:

data:function(){
     return {
           title:"hello vue"
     }
 }

8.2 组件的生命周期

Vue中的组件也是有生命周期的。一个Vue组件会经历:初始化、创建、绑定、更新、销毁等阶段,不同的阶段,都会有相应的生命周期钩子函数被调用。

组件的生命周期钩子

    生命周期主要有三个阶段:

  • 一,初始化显示;(重要勾子 mounted 网页加载完毕触发)
  • 二,更新显示;(重要勾子beforeUpdate 数据属性更新前)
  • 三,死亡;(重要勾子beforeDestroy vm死亡前)

   每个生命周期都会有对应的生命周期的函数,或者叫做勾子函数;

<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
</head>
<body>
    <pre>每个 Vue 实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、
        将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,
        这给了用户在不同阶段添加自己的代码的机会。</pre>
    <div id="app">
        <p>{{name}}</p>
        <button @click="update">update</button>
        <button @click="destroy">destroy</button>
    </div>
<script type="text/javascript">
    new Vue({
        el:"#app",
        data:{
            a:1,
            name:"zs"
        },
        methods:{
            update(){
                this.name = Math.random() +"---";
            },
            destroy(){
                this.$destroy();
            }
        },
        beforeCreate(){
          console.log("创建vue实例之前");
        },
        created(){
            //
            console.log("vue实例创建成功");
            console.log(this.a);
        }
       /* created: () => console.log(this.a)*/
        ,
        beforeMount(){
            console.log("vue对象挂载之前");
        },
        mounted(){
            // 只执行一次    页面加载完之后执行       比如 咱们在 页面加载完毕 发送ajax请求 获取数据
            console.log(" mounted   挂载完毕")
        },
        beforeUpdate(){
            // 可执行多次
            console.log("data更新之前执行");
        },
        updated(){
            // 可执行多次
            console.log("data更新之后执行");
        },
        beforeDestroy(){
            // 只执行一次
            console.log("vue实例销毁之前执行");
        },
        destroyed(){
            // 只执行一次
            console.log("vue实例销毁之后执行");
        }
​
    });
</script>
</body>
</html>
​

九、使用Vue-Cli搭建Vue项目

9.1 什么是vue-cli

cli: Command Line 命令行工具,vue-cli就是vue的命令行工具,也称之为脚手架,使用vue-cli提供的各种命令可以拉取、创建、运行我们需要使用到的框架,比如webpack、Element UI、Element Admin等等。那么要想使用vue-cli命令,需要先安装node.js。

9.2 node.js的介绍及安装

node.js的介绍

node.js提供了前端程序的运行环境,可以把node.js理解成是运行前端程序的服务器。

node.js的安装

从官网下载安装即可: 下载 | Node.js 中文网

测试node.js是否安装成功: 在DOS窗口中输入“node -v” 查看版本,如果看到版本,就表示安装成功。

9.3 使用node.js安装vue-cli

使用如下命令安装vue-cli

npm install -g vue-cli   安装的是2.9.6 版本     -------   不装这个
npm install  -g   @vue/cli     安装的是新版本   新版本  支持 vue ui 
  • npm: 使用node.js的命令

  • install: 安装

  • vue-cli: 要安装的vue-cli

  • -g: 全局安装

注意 这里的安装可能需要管理员权限

安装完了之后 可能被windows的安全策略限制 参考

VSCode报错:vue : 无法加载文件 D:\nodejs\node_global\vue.ps1,因为在此系统上禁止运行脚本。_没有咸鱼的梦想的博客-CSDN博客

当出现以下界面,表示正在安装:

如果使用npm官方镜像速度比较慢,可以使用淘宝镜像来安装:

npm install -g cnpm --registry=https://registry.npm.taobao.org

之后使用npm命令时就可以替换成cnpm

cnpm install vue-cli -g
cnpm install
cnpm run dev

9.4创建vue项目

dos命令输入vue ui会进入如下图:

创建项目 不保存预设 然后 后台就开始下载文件

创建好之后 页面跳转到仪表盘

可以在这里 安装一些插件 比如 axios element-ui 等等

这是已经安装的插件

点 添加插件

安装 ajax 插件 axios

然后 可以关掉仪表盘,使用vscode 打开 刚才创建的工程 使用 vue run serve 启动工程,

9.4.1配置vue片段

打开VSCode,文件—>首选项—>配置用户代码片段 

输入一下代码:配置完成后新建.vue文件输入vue回车就会生成以下代码。

{
	"Print to console": {
			"prefix": "vue",
			"body": [
					"<!-- $1 -->",
					"<template>",
					"  <div class='$2'>$5</div>",
					"</template>",
					"",
					"<script>",
					"//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)",
					"//例如:import 《组件名称》 from '《组件路径》';",
					"",
					"  export default {",
					"    //import引入的组件需要注入到对象中才能使用",
					"    components: {},",
					"    props: {},",
					"    data() {",
					"      //这里存放数据",
					"      return {}",
					"    },",
					"    //监听属性 类似于data概念",
					"    computed: {},",
					"    //监控data中的数据变化",
					"    watch: {},",
					"    //方法集合",
					"    methods: {},",
					"    //生命周期 - 创建完成(可以访问当前this实例)",
					"    created() {},",
					"    //生命周期 - 挂载完成(可以访问DOM元素)",
					"    mounted() {},",
					"    beforeCreate() {}, //生命周期 - 创建之前",
					"    beforeMount() {}, //生命周期 - 挂载之前",
					"    beforeUpdate() {}, //生命周期 - 更新之前",
					"    updated() {}, //生命周期 - 更新之后",
					"    beforeDestroy() {}, //生命周期 - 销毁之前",
					"    destroyed() {}, //生命周期 - 销毁完成",
					"    activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发",
					"  }",
					"</script>",
					"<style scoped>",
					" /* @import url(); 引入公共css类 */",
					"$4",
					"</style>"
			],
			"description": "生成vue模板"
	}
}

9.5 使用vue-cli下载项目骨架搭建我们的项目

就像maven一样,vue为我们提供了一些官方项目骨架。使用vue list命令可以列出当前官方提供的骨架,可以使用这些骨架来快速搭建出项目。

vue list

完事 用vscode 打开项目文件夹

9.6vue工程的组件使用

9.7父组件向子组件传递数据---父子组件之间

父组件

消息订阅与发布组件PubSub

9.8 消息订阅与发布组件PubSub

我们前面讲了父子组件之间通过prop来实现消息传递;但是再其他情况,比如兄弟组件,爷孙组件消息传递时候,就要用到高级的消息订阅与发布;

1.首先我们安装下消息订阅与发布pubsub组件;

npm install --save pubsub-js

2.把 pubsub 配置成全局组件 在 main.js 中配置

3.在 父组件中 订阅消息 可以在 mounted 钩子函数中 监听消息

在这里 使用了 箭头函数 => ,在箭头函数中 的 this 代表 当前 vue 对象,如果 使用一般函数 ,那么在这个函数内 this 就代表PubSub对象,所以 在这里 咱们使用 =>箭头函数

在别的组件中 发布消息

注意: 执行上面的案例 会发现 订阅消息的回调函数 执行了两次 , 我们需要在我们每次接受数据pubsub.subscribe的时候,先执行pubsub.unsubscribe操作就好了,就完美解决了,这样你接收以后的callback只执行一次

PubSub.unsubscribe();
PubSub.subscribe(eventName, callback);   //这样  回调就只执行一次

9.9插槽 slot

父组件向子组件传递标签,通过slot 插槽实现

主要作用:

某一个区块,先占位,然后可以动态的搞个标签进去,方便切换该位置的内容,无需再搞多个页面。

父组件 直接定义标签p 注意 slot 属性

子组件 使用 slot 标签 占位置 注意 slot 标签的name 属性,当父组件没有传对应的插槽(属性值对应)内容时,会显示插槽默认内容

9.10 使用Webpack骨架快速创建项目

常用命令:

  • npm install

    在运行和调试项目前,一般都需要先执行该命令,目的是安装项目运行所需要的环境。

  • npm run dev

    以调试的方式运行项目

  • npm run build

    生成用于项目部署所需的最小资源,生成的内容存放在build文件夹内。

Webpack是一个前端资源的打包工具,它可以将js、image、css等资源当成一个模块进行打包。

从图中我们可以看出,Webpack可以将js、css、png等多种静态资源进行打包,使用webpack有什么好处呢?

1、模块化开发程序员在开发时可以分模块创建不同的js、css等小文件方便开发,最后使用webpack将这些小文件打包成一个文件,减少了http的请求次数。webpack可以实现按需打包,为了避免出现打包文件过大可以打包成多个文件。

2、编译typescript、ES6等高级js语法随着前端技术的强大,开发中可以使用javascript的很多高级版本,比如:typescript、ES6等,方便开发,webpack可以将打包文件转换成浏览器可识别的js语法。

3、CSS预编译webpack允许在开发中使用Sass和Less等原生CSS的扩展技术,通过sass-loader、less-loader将Sass和Less的语法编译成浏览器可识别的css语法

4.使用vue.js开发大型应用需要使用webpack打包工具

vue init webpack my-project1
  • webpack: 骨架名称

  • my-project1: 项目名称 

过程中会出现如下界面,需要手动操作。
出现如下界面,表示安装成功。

进入到my-project1文件夹内后,使用以下命令来运行项目。

npm run dev

访问http://localhost:8081,页面效果如下:

 十、Vue-router 路由

10.1 安装路由模块

npm install vue-router --save 

10.2 引入路由模块并使用

创建views 文件夹 其下面创建Index.vue 和 Menu1.vue

在main.js中引入路由模块并使用

import Vue from 'vue'
import App from './App'
import router from './router'  //引入路由模块
​
Vue.config.productionTip = false
​
/* eslint-disable no-new */
new Vue({
  el: '#app',
  router, //使用路由模块
  components: { App },
  template: '<App/>'
})

 在 router文件夹下 的index.js 是 路由的配置文件

index.js中:

import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
import Index from '@/views/Index'
import Menu1 from '@/views/Menu1'
Vue.use(Router)
​
export default new Router({
  routes: [
    {
      path: '/',
      name: 'HelloWorld',
      component: HelloWorld
    },
    {
      path: '/index',
      name: 'Index',
      component: Index
    },
    {
      path: '/menu1',
      name: 'Menu1',
      component: Menu1
    }
  ]
})
​

在App.vue 中的路由写法

 <div class="menu">
      <ul>
        <li>
          <!--<a href="">首页</a>-->
          <router-link to="/index">首页</router-link>
        </li>
        <li>
          <!--<a href="">菜单1</a>-->
          <router-link to="/menu1">菜单1</router-link>
        </li>
      </ul>
    </div>
    <div class="content">
      <router-view></router-view>
    </div>

通过 router-link 向路由配置发送请求 ,结果会在 router-view 显示

10.3嵌套路由

Menu1.vue是在前面创建的views文件夹下 继续嵌套 下级页面 配置嵌套路由,其他代码有vue片段生成

Menu1.vue

<div>
      {{msg}}
      <ul>
          <li>
            <router-link to="/menu1/submenu1">子菜单1</router-link>
          </li>
          <li>
            <router-link to="/menu1/submenu2">子菜单2</router-link>
          </li>
        </ul>
​
        <div class="content">
                <router-view></router-view>
      </div>
​
  </div>

在路由配置文件src文件夹下router文件夹下的index.js中

{
      path: '/menu1',
      name: 'Menu1',
      component: Menu1,
      children:[   // 子路由
        {
          path:'/menu1/submenu1',    // 当然  对应的 子组件 得创建好
          component: () => import('@/views/SubMenu1.vue')   // 这样就不用在文件上面配置了
        },
        {
          path:'/menu1/submenu2',
          component: () => import('@/views/SubMenu2.vue')
        },
        {
          path:'',
          redirect:'/menu1/submenu1'     // 默认显示submenu1
        }
      ]
    },

在views文件夹下创建SubMenu1.vue,其他代码vue片段生成

<template>
  <div>子菜单1
      <input type="text" name="sub1input">
  </div>
</template>

SubMenu2.vue,其他代码vue片段生成

<template>
  <div>子菜单2
      <input type="text" name="sub2input">
  </div>
</template>

10.4路由缓存

当我们 在 子菜单1 和 子菜单2 的 文本输入框输入内容时,发现 跳转之后内容没有了,这种情况也经常遇到

我们切换不同的组件时,希望 组件内输入的内容不要丢失 ,怎么解决?只需要 使用keep-alive 标签

10.5路由传参

app.vue显示页面的配置:

<template>
  <div id="app">
    <div class="menu">
      <ul>
        <li>
          <router-link to="/">首页</router-link>
        </li>
          <li>
          <router-link to="/menu1">菜单1</router-link>
        </li>
        <li>
          <router-link to="/admin">账户信息</router-link>
        </li>
      </ul>
    </div>
    <div class="content">
      <router-view></router-view>
    </div>
  </div>
</template>
<script>
export default {
  mounted(){
    this.axios.defaults.headers.common['token'] = 6666;
  }
}
</script>
<style>
ul li{
  float: left;
  margin-right: 20px;
  list-style: none;
}
.content{
  clear: both;
}
</style>

注意:上述路由需要再定义个div并在里面写<reouter-view>标签

router文件下index.js中路由配置,这里只设置了账户信息路由

{
      //path对应app.vue文件中账户信息标签to="/admin"
      path: '/admin', 
      name: 'index',
      component: () => import ('@/views/Admin.vue'),
      children: [ // 子路由,点击姓名显示详细信息
        {
          path:'/admin/adminDetail/:id',
          component: () => import ('@/views/AdminDetail.vue')
        }
      ]
    },

用户信息组件views文件夹下创建Admin.vue

<template>
  <div class="">
    <table>
      <tr>
        <th>编号</th>
        <th>账号</th>
        <th>密码</th>
      </tr>
      <tr v-for="(admin, index) in admins" v-bind:key="index">
        <td>{{ admin.id }}</td>
        <router-link :to='`/admin/adminDetail/${admin.id}`'><td>{{ admin.username }}</td></router-link>
        <td>{{ admin.password }}</td>
      </tr>
    </table>
    <hr>
    <div class="content">
        <router-view></router-view>
    </div>
  </div>
</template>
​
<script>
​
export default {
    //这里存放数据
  data() {
    return {
      admins: [],
    };
  },
​
  //方法集合
  methods: {
    getAdmins() {
      this.axios({
        url: "http://localhost:8081/admin/list",
        params: {
          page: 1,
          limit: 10,
        },
      })
        .then((res) => {
          this.admins = res.data.data;
        })
        .catch(function (error) {
          //处理错误情况
          console.log(error);
        })
        .then(function () {
          //总是会执行
        });
    },
  },
​
  //生命周期 - 挂载完成(可以访问DOM元素)
  mounted() {
    this.axios.defaults.headers.common['token'] = 6666;
    this.getAdmins(); // 页面加载完成后调用这个函数
  },
};
</script>
<style>
table {
  border: 1px solid black;
}
th,
td,
tr {
  border: 1px solid black;
  text-align: center;
  width: 150px;
}
/* @import url(); 引入公共css类 */
</style>

当点击名字时会显示该用户信息,用到嵌套路由,在views下创建AdminDetail.vue文件,代码如下:

<template>
  <div class="">
    Id:{{ $route.params.id }}<br />
    {{ admin.username }}<br />
    {{ admin.password }}<br />
    {{ admin.type }}<br />
    {{ admin.create_time }}<br />
  </div>
</template>
​
<script>
export default {
  data() {
    //这里存放数据
    return {
      admin: {},
    };
  },
  //监控data中的数据变化
  watch: { 
    $route(value) {
      let id = value.params.id;
      this.getAdminById(id); // 每次点击姓名都会调用该方法
    },
  },
  //方法集合
  methods: {
    getAdminById(id) {
      console.log(id);
      this.axios({
        url: `http://localhost:8081/admin/adminDetail/${id}`,
      })
        .then((res) => {
          console.log("响应的id详情");
          console.log(res.data.data.admin);
          this.admin = res.data.data.admin;
        })
        .catch((error) => {
          console.log(error);
        });
    },
  },
  //生命周期 - 挂载完成(可以访问DOM元素)
  mounted() {
    let id = this.$route.params.id;
    this.getAdminById(id);
  },
};
</script>

10.6 编程式路由传参

只需要在上面的Admin.vue文件中添加一些代码就行。

<template>
  <div class="">
    <table>
      <tr>
        <th>编号</th>
        <th>账号</th>
        <th>密码</th>
        <th>测试编程式路由</th>
      </tr>
      <tr v-for="(admin, index) in admins" v-bind:key="index">
        <td>{{ admin.id }}</td>
        <router-link :to="`/admin/adminDetail/${admin.id}`"
          ><td>{{ admin.username }}</td></router-link
        >
        <td>{{ admin.password }}</td>
        <td>
          <button @click="pushShow(admin.id)">push</button>
          <button @click="replaceShow(admin.id)">replace</button>
        </td>
      </tr>
    </table>
    <button @click="$router.back()">回退</button>
    <hr />
    <div class="content">
      <router-view></router-view>
    </div>
  </div>
</template>
​
<script>
export default {
  //这里存放数据
  data() {
    return {
      admins: [],
    };
  },
​
  //方法集合
  methods: {
    getAdmins() {
      this.axios({
        url: "http://localhost:8081/admin/list",
        params: {
          page: 1,
          limit: 10,
        },
      })
        .then((res) => {
          this.admins = res.data.data;
        })
        .catch(function (error) {
          //处理错误情况
          console.log(error);
        })
        .then(function () {
          //总是会执行
        });
    },
    pushShow(id) {
      console.log(id);
      // 点击时路由带id,通过$router获得id
      this.$router.push(`/admin/adminDetail/${id}`);
    },
    replaceShow(id) {
      this.$router.replace(`/admin/adminDetail/${id}`);
    },
  },
​
  //生命周期 - 挂载完成(可以访问DOM元素)
  mounted() {
    this.axios.defaults.headers.common["token"] = 6666;
    this.getAdmins(); // 页面加载完成后调用这个函数
  },
};
</script>
<style>
table {
  border: 1px solid black;
}
th,
td,
tr {
  border: 1px solid black;
  text-align: center;
  width: 150px;
}
/* @import url(); 引入公共css类 */
</style>

 十一、使用Axios发送请求

11.1 什么是 Axios

Axios 是一个开源的可以用在浏览器端和 NodeJS 的异步通信框架,她的主要作用就是实现 AJAX 异步通信,其功能特点如下:

  • 从浏览器中创建 XMLHttpRequests

  • 从 node.js 创建 http 请求

  • 支持 Promise API

  • 拦截请求和响应

  • 转换请求数据和响应数据

  • 取消请求

  • 自动转换 JSON 数据

  • 客户端支持防御 XSRF(跨站请求伪造)

GitHub:GitHub - axios/axios: Promise based HTTP client for the browser and node.js

11.2 为什么要使用 Axios

由于 Vue.js 是一个 视图层框架 并且作者(尤雨溪)严格准守 SoC (关注度分离原则),所以 Vue.js 并不包含 AJAX 的通信功能,为了解决通信问题,作者单独开发了一个名为 vue-resource 的插件,不过在进入 2.0 版本以后停止了对该插件的维护并推荐了 Axios 框架

11.3 Axios的使用

11.3.1 安装vue axios

npm install --save axios vue-axios

11.3.2 在main.js中引入

在项目中使用axios模块

import Vue from 'vue'
import axios from 'axios'
import VueAxios from 'vue-axios'
​
Vue.use(VueAxios, axios)

11.4 发送axios请求

在使用axios时,要注意到配置选项中包含params和data两者

1.params是添加到url的请求字符串中的,用于get请求;

2.而data是添加到请求体(body)中的, 用于post请求;

11.4.1axios使用方法-post

this.$axios({  //这种格式,是需要每个参数都弄成对象的
        methods: 'POST',
        url: '#',
        data: {
          key1: val1,
          key2: val2
        },
        timeout: 1000,
        baseURL:'xxxx'
        ...//其他相关配置
      })

11.4.2axios使用方法-get

  this.$axios({
        methods: 'get',
        url: '#',
        params: {  //注意是params,get请求必须写上params,不能写data
          key1: val1,
          key2: val2
        },
        timeout: 1000,
        baseURL:'xxxx'
        ...//其他相关配置
      })

注意:

在get请求下,参数需要使用【params】来设置,

而post请求中,是使用data来传递的

11.4.3axios的简化写法

this.$axios.get('url')
this.$axios.post('url')

注意:这种直接没有参数的,就写个url就行了

get请求直接写参数是错误写法,会导致请求无法携带参数

如果get请求有参数,必须使用params:{}括起来

11.4.4axios请求案例

如果带token的话可以在app.vue中的mounted里面设置全局的

 mounted(){
    this.axios.defaults.headers.common['token'] = 6666;
  }

创建一个.vue文件书写一下代码:

<template>
  <div class="">
    <table>
      <tr>
        <th>编号</th>
        <th>账号</th>
        <th>密码</th>
      </tr>
      <tr v-for="(admin, index) in admins" v-bind:key="index">
        <td>{{ admin.id }}</td>
        <td>{{ admin.username }}</td>
        <td>{{ admin.password }}</td>
      </tr>
    </table>
  </div>
</template>
​
<script>
export default {
  data(){
      data() {
    //这里存放数据
    return {
      admins: [],
    };
  },
​
  },
  methods:{
    getAdmins() {
      this.axios({
        methods:"get",
        url: "http://localhost:8081/admin/list",
        params: {
          page: 1,
          limit: 10,
        },
      })
        .then((res) => {
          this.admins = res.data.data;
        })
        .catch(function (error) {
          //处理错误情况
          console.log(error);
        })
        .then(function () {
          //总是会执行
        });
    } 
    },
      mounted() {
    this.axios.defaults.headers.common['token'] = 6666;
    this.getAdmins(); // 页面加载完成后调用这个函数
  },  
  }
}
</script>

11.4.5 服务端解决跨域问题

<mvc:cors>  
    <mvc:mapping path="/"
        allowed-origins="*"
        allowed-methods="POST, GET, OPTIONS, DELETE, PUT,PATCH"
        allowed-headers="Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With"
        allow-credentials="true" />
</mvc:cors>

在spring-mvc.xml中加入上述这一段。其中,allowed-origins指的是允许的访问源的域名,"*"表示任何人都可以访问,也可以指明具体的域名

11.4.6解决axios无法传递data中的参数问题

原因:默认情况下发送axios时请求头中的内容类型为: (后端没有使用@RequestBody)

Content-Type:application/json;charset=UTF-8

而实际服务端需要的是:

Content-Type:application/x-www-form-urlencoded

因此,使用axios的qs内置库中的方法进行内容类型的转换。

import Qs from 'qs'
​
this.axios({
    method:'post',
    url:'http://localhost:8081/regist',
    transformRequest: [function (data) {
        return Qs.stringify(data)
    }],
    data:{
        email:this.email
    }
})
.then(function (response) {
    alert(response.data.message)
});

十二、 Vuex状态集中式管理

12.1 什么是Vuex

Vuex 是一个专为 Vue.js 应用程序开发的 状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

12.2步骤

1.先在创建一个没有使用 vuex的组件 MyVuex.vue

<template>
  <div class=''>
    vuex
    <p>当前数字: {{number}} is {{evenOrOdd}}</p>
    <button v-on:click="jia()">加</button>
    <button @click="jian()">减</button>
  </div>
</template>
<script>
  export default {
    data() {
      //这里存放数据
      return {
        number:0
      }
    },
    //监听属性 类似于data概念
    computed: {
        evenOrOdd(){
            return this.number%2==0?'偶数':'奇数'
        }
    },
    //方法集合
    methods: {
        jia(){
            this.number++;
        },
        jian(){
            this.number--;
        }
    },
  }
</script>

 12.3 安装

1.如果不想安装可以在构建项目时勾选此选项: 

 

2.也可以在项目根目录执行如下命令来安装 Vuex

若失败,可使用cnpm

npm install vuex --save

修改 main.js 文件,导入 Vuex,关键代码如下:

import Vuex from 'vuex'
Vue.use(Vuex);
npm install vuex --save

但是 如果项目里 需要多个组件 共享这个 number 的值 ,也就是 number 的变化 很多组件都能感知到 怎么实现,vuex 状态集中式管理 就是解决这个问题的。

首先 安装

修改 main.js 文件,导入 Vuex,关键代码如下:

import Vuex from 'vuex'
Vue.use(Vuex);

创建vuex配置文件 在 src 目录下创建一个名为 store 的目录并新建一个名为 index.js 文件用来配置 Vuex,代码如下:

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
​
export default new Vuex.Store({
    // 全局 state 对象,用于保存所有组件的公共数据
    state: {
        //在组件中是通过 this.$store.state.number 来获取
        number: 0
    },
    // 实时监听 state 值的最新状态,注意这里的 getters 可以理解为计算属性
    getters: {
        // 在组件中是通过 this.$store.getters.evenOrOdd 来获取
        evenOrOdd(state) {
            return state.number % 2 == 0 ? '偶数' : '奇数'
        }
    },
    // 定义改变 state 初始值的方法,这里是唯一可以改变 state 的地方,缺点是只能同步执行
    mutations: {
        // 在组件中是通过 this.$store.commit('jia'); 方法来调用 mutations
        jia(state) {
            state.number++
        },
        jian(state) {
            state.number--
        }
    },
    // 定义触发 mutations 里函数的方法,可以异步执行 mutations 里的函数
    actions: {
​
        // 在组件中是通过 this.$store.dispatch('jia'); 来调用 actions
        jia(context) {
            context.commit('jia')
        },
        // 在组件中是通过 this.$store.dispatch('jian'); 来调用 actions
        jian({ commit, state }) {    //解构 context 对象  可以拿到对象属性  commit  和  state
            if (state.number > 0) {
                commit('jian')
            }
        }
    },
    modules: {
​
    }
})

修改 main.js 增加刚才配置的 store/index.js,关键代码如下:

import Vue from 'vue'
import Vuex from 'vuex'
import store from './store'
​
Vue.use(Vuex);
​
new Vue({
  el: '#app',
  store
});

修改MyVuex.vue

<p>当前数字: {{$store.state.number}} is {{$store.getters.evenOrOdd}}</p>
    <button v-on:click="jia()">加</button>
    <button @click="jian()">减</button>
    methods: {
        jia(){
            // this.$store.commit('jia'); // 同步调用
            this.$store.dispatch('jia'); // 异步调用
        },
        jian(){
          // this.$store.commit('jian'); // 同步调用
          this.$store.dispatch('jian'); // 异步调用
        }
    },

依然 可以实现之前的效果

但是感觉 每次 都写 $.store.xxx.xxx 太长了 ,怎么办?

// 不想每次都写 $store.state.xxx    就导入组件然后计算属性    
//  第一步:在MyVuex.vue中的<script>标签下导入
import { mapState, mapGetters, mapActions } from "vuex";
//计算属性 类似于data概念
computed: {
    // evenOrOdd(){
    //     return this.number%2==0?'偶数':'奇数'
    // }
​
    // 第二步
    ...mapState(['number']),
    ...mapGetters(['evenOrOdd'])  // 如果 名字不一致
​
    //...mapGetters({'evenOrOdd':'evenOrOdd222'})    // 如果 名字不一致  vuex 中的 getters 内部的方法名 为 evenOrOdd222   不建议这样写
},
methods: {
    
    // jia(){
    //     this.number++
    // },
    // jian(){
    //     this.number--
    // }
​
    //jia(){
    //    this.$store.dispatch('jia')
    //},
    //jian(){
    //     this.$store.dispatch('jian')
    //}
    ...mapActions(['jia','jian'])
},
<div>
     <!-- <p>当前数字 {{$store.state.number}} is {{$store.getters.evenOrOdd}}</p>      -->
​
    <!-- // 第三步  -->
    <p>当前数字: {{number}} is {{evenOrOdd}}</p>
    <button v-on:click="jia()">加</button>
    <button @click="jian()">减</button>
</div>

12.4解决浏览器刷新Vuex 数据消失问题

  • 问题描述

Vuex 的状态存储是响应式的,当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。但是有一个问题就是:vuex 的存储的数据只是在页面的中,相当于我们定义的全局变量,刷新之后,里边的数据就会恢复到初始化状态。但是这个情况有时候并不是我们所希望的。 

  • 解决方案

监听页面是否刷新,如果页面刷新了,将 state 对象存入到 sessionStorage 中。页面打开之后,判断 sessionStorage 中是否存在 state 对象,如果存在,则说明页面是被刷新过的,将 sessionStorage 中存的数据取出来给 vuex 中的 state 赋值。如果不存在,说明是第一次打开,则取 vuex 中定义的 state 初始值。

  • 修改代码

在 App.vue 中增加监听刷新事件

  export default {
    name: 'App',
    mounted() {
      window.addEventListener('unload', this.saveState);
    },
    methods: {
      saveState() {
        sessionStorage.setItem('state', JSON.stringify(this.$store.state));
      }
    }
  }

修改 store/index.js 中的 state

// 设置刷新浏览器number值不改变
state: sessionStorage.getItem('state') ? JSON.parse(sessionStorage.getItem('state')) : {
  user: {
    name: ''
  }
},

思考: 此时 复制 MyVuex.vue 组件 复制出来一个新的 MyVuex2.vue , 访问这两个 页面 为什么 number 的值 却没有共享?

vuex 和 vuex 2 中 number 的值怎么不一致? 注意 这是两个窗口 相当于是两个人,所以不一致, 再试试在一个窗口 进行页面跳转 就会发现一致了

十三、Vue实战增删改查上传文件

工具:前端VSCode,后端Idea,数据库MySql

13.1前端vue的js配置文件

相关配置.js文件配置

13.1.1src > man.js文件

main.js是项目的入口文件,项目中所有的页面都会加载main.js,所以main.js,主要有三个作用:

  1. 实例化Vue。
  2. 放置项目中经常会用到的插件和CSS样式。例如: 网络请求插件:axios和vue-resource、图片懒加载插件:vue-lazyload
  3. 存储全局变量。例如(用于的基本信息)
import Vue from 'vue'
import App from './App'
import router from './router'  //引入路由模块
import axios from 'axios'
import VueAxios from 'vue-axios'
import Vuex from 'vuex'
import store from './store'
import ElementUI from 'element-ui'; // 引入element-ui
import 'element-ui/lib/theme-chalk/index.css';  // element-ui的css样式要单独引入

// Vue.prototype.$axios = axios
Vue.config.productionTip = false
Vue.use(VueAxios, axios)
Vue.use(Vuex)
Vue.use(ElementUI);   // 这种方式引入了ElementUI中所有的组件

/* eslint-disable no-new */
new Vue({
  store,
  el: '#app',
  router, //使用路由模块
  components: { App },
  template: '<App/>'
})

13.1.2src > router > index.js文件

该文件用来配置路由,路由用于设定访问路径,并将路径和组件映射起来,路由实际上就是可以理解为指向,就是我在页面上点击一个按钮需要跳转到对应的页面,这就是路由跳转。

import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)

export default new Router({
  routes: [
    // 默认跳转登录页面
    {
      path: '/',
      name: 'login',
      component: () => import('@/pages/Login.vue')
    },
    
    // 跳转home主页面
    {
      path: '/home',
      name: 'home',
      component: () => import('@/pages/Home.vue'),
      children: [

        // 跳转用户信息页面
        {
          path: '/home/adminList',
          component: () => import('@/pages/AdminList.vue'),
          children: [

            // 跳转增加用户页面
            {
              path: '/home/adminAdd',
              component: () => import('@/pages/AdminAdd.vue')
            },

            // 跳转更新用户页面
            {
              path: '/home/adminEdit',
              component: () => import('@/pages/AdminEdit.vue')
            },

            //跳转详情页面
            {
              path: '/home/adminDetail',
              component: () => import('@/pages/AdminDetail.vue')
            }
          ]
        },
      ]
    },
  ]
})

13.1.3src > store > index.js文件

store用于管理状态共享数据以及在各个组件之间管理外部状态,store是vuex应用的核心,也就是一个容器,包含着应用中大部分的状态,更改store中的状态唯一方法是提交mutation。本案例用来展示用户名。

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);

export default new Vuex.Store({
    // 全局 state 对象,用于保存所有组件的公共数据
    // 设置刷新浏览器username值不改变
    state: sessionStorage.getItem('state') ? JSON.parse(sessionStorage.getItem('state')) : {
        admin:{username:'',}
      },

    // 实时监听state值的最新状态,注意这里的getters可以理解为计算属性
    getters: {
        //上面的username一个计算属性
        getAdmin(state){
            return state.admin;
        }
    },

    // 定义改变state初始值的方法,这里是唯一可以改变state的地方,缺点是只能同步执行
    mutations: {
        // 在组件中是通过this.$store.commit('updateAdmin')方法来调用mutations
        updateAdmin(state,admin){
            //把admin用户放到state中,也就是放到全局里面
            state.admin = admin;
        },
    },

    // 定义触发mutations里函数的方法,可以异步执行mutations里的函数
    actions: {
        // 在组件中是通过 this.$store.dispatch('asyncUpdateAdmin')来调用 actions
        //取出登录成功时存进去的当前用户信息
        asyncUpdateAdmin(context,admin){
            context.commit("updateAdmin",admin)
        }, 
    },
})

13.1.4src > components >

upload >policy.js文件

该文件用来配置文件上传前端请求后台签名接口,此次上传方式采用前端请求后台签名,然后前端凭借签名上传文件到OSS,不再用后端去上传文件到OSS,让前端来做。

// import request from '@/utils/request'
/**
 * 上传文件时请求后台获取凭证
 */
export function policy(_self){
  return new Promise((resolve,reject) => {

    //请求后台签名接口,拿到签名数据
    _self.axios.get("http://localhost:8080/oss/policy",{}).then(({data}) => {
      resolve(data);
    })
  })
}

13.2前端Vue页面及代码

13.2.1App.vue代码

<template>
  <div id="app">
    <div class="content">
      <router-view></router-view>
    </div>
  </div>
</template>

<script>
//导入组件
export default {
  mounted(){
    // // 监听页面属性 页面刷新调用
    // window.addEventListener('unload', this.saveState);
  },
}
</script>
<style>
</style>

13.2.2登录页面及代码

在src下创建pages文件夹,然后创建Login.vue 

<template>
  <div class="login-box">
    <el-form ref="form" :model="form" :rules="rules" label-width="80px">
        <h3 style="font-family:楷体">欢迎登录</h3>
        <el-form-item label="用户名" prop="username">
            <el-input v-model="form.username" placeholder="请输入用户名"></el-input>
        </el-form-item>
        <el-form-item label="密码" prop="password">
            <el-input type="password" v-model="form.password" placeholder="请输入密码"></el-input>
        </el-form-item>
        <el-form-item>
            <el-button type="primary" @click="onSubmit('form')">登录</el-button>
        </el-form-item>
    </el-form>
  </div>
</template>

<script>
  export default {
    //import引入的组件需要注入到对象中才能使用
    components: {},
    props: {},
    data() {
      //这里存放数据
      return {
        form: {
            username:"",
            password:"",
        },
        
        // 表单校验
        rules: {
            username:[
                { required:true,message:"请输入用户名",trigger:"blur"},
                {
                    min:3,
                    max:20,
                    message:"长度在3到20个字符",
                    trigger:"blur",
                },
            ],
            password:[
                {required:true,message:"请输入密码",trigger:"blur"},
                {
                    min:3,
                    max:20,
                    message:"长度在3到20个字符",
                    trigger:"blur",
                },
            ],
        },
      };
      
    },
    //监听属性 类似于data概念
    computed: {},
    //监控data中的数据变化
    watch: {},
    //方法集合
    methods: {
        onSubmit(formName) {
            this.$refs[formName].validate((valid) => {
                var vm = this;
                console.log("这是vm")
                console.log(vm.form.username)
                if(valid) {
                    console.log("表单验证通过");
                    // 发送axios请求
                    this.axios({
                        method:"get",
                        url:"http://localhost:8080/admin/login",
                    
                        params:{
                            // 获取的输入框的username值
                            username:vm.form.username, 
                            
                            // 获取的输入框的password值
                            password:vm.form.password, 
                        }
                    }).then(function (resp){
                        console.log(resp.data)
                        // debugger
                        if(resp.data.code == 200){
                            //登录成功,要向vuex中存放user对象
                            var admin = resp.data.data.currentAdmin;
                            
                            //登录成功将token放入本地存储
                            var token = resp.data.data.token;
                            window.localStorage.setItem("token",token);

                            //把登陆成功用户信息存到state中
                            console.log(admin)
                            vm.$store.dispatch("asyncUpdateAdmin",admin);

                            vm.$message({
                                message:"登录成功",
                                type:"success",
                            });
                            setTimeout(function(){
                                //跳转到首页
                                vm.$router.push("/home");
                            },2000);
                        } else {
                            vm.$message.error("用户名或密码错误");
                        }
                    });
                } else {
                    console.log("表单验证失败");
                }
            })
        }
    
    },
  }
</script>
<style scoped>
 /* @import url(); 引入公共css类 */
 .login-box {
    width: 500px;
    height: 300px;
    border:1px solid #dcdfe6;
    margin: 150px auto;
    padding: 20px 50px 20px 30px;
    border-radius: 20px;
    box-shadow: 0px 0px 20px #dcdfe6;
 }
</style>

13.2.3主页面及代码

在src下的pages文件夹下面创建Home.vue 

<template>
  <el-container style="height: 703px; border: 1px solid #eee">
    <el-aside width="200px" style="background-color: rgb(238, 241, 246)">
      <el-menu :default-openeds="['1', '3']">
        <el-submenu index="1">
          <template slot="title"
            ><i class="el-icon-message"></i>导航一</template
          >
          <el-menu-item-group>
            <template slot="title">分组一</template>
            <el-menu-item index="1-1">
              <router-link to="/home/adminList">用户管理</router-link>
            </el-menu-item>
            <el-menu-item index="1-2">
              <router-link to="/home/roomList">房屋管理</router-link>
            </el-menu-item>
          </el-menu-item-group>
          <el-menu-item-group title="分组2">
            <el-menu-item index="1-3">选项3</el-menu-item>
          </el-menu-item-group>
          <el-submenu index="1-4">
            <template slot="title">选项4</template>
            <el-menu-item index="1-4-1">选项4-1</el-menu-item>
          </el-submenu>
        </el-submenu>
        <el-submenu index="2">
          <template slot="title"><i class="el-icon-menu"></i>导航二</template>
          <el-menu-item-group>
            <template slot="title">分组一</template>
            <el-menu-item index="2-1">选项1</el-menu-item>
            <el-menu-item index="2-2">选项2</el-menu-item>
          </el-menu-item-group>
          <el-menu-item-group title="分组2">
            <el-menu-item index="2-3">选项3</el-menu-item>
          </el-menu-item-group>
          <el-submenu index="2-4">
            <template slot="title">选项4</template>
            <el-menu-item index="2-4-1">选项4-1</el-menu-item>
          </el-submenu>
        </el-submenu>
        <el-submenu index="3">
          <template slot="title"
            ><i class="el-icon-setting"></i>导航三</template
          >
          <el-menu-item-group>
            <template slot="title">分组一</template>
            <el-menu-item index="3-1">选项1</el-menu-item>
            <el-menu-item index="3-2">选项2</el-menu-item>
          </el-menu-item-group>
          <el-menu-item-group title="分组2">
            <el-menu-item index="3-3">选项3</el-menu-item>
          </el-menu-item-group>
          <el-submenu index="3-4">
            <template slot="title">选项4</template>
            <el-menu-item index="3-4-1">选项4-1</el-menu-item>
          </el-submenu>
        </el-submenu>
      </el-menu>
    </el-aside>

    <el-container>
      <el-header style="text-align: right; font-size: 12px">
        <el-dropdown>
          <i class="el-icon-setting" style="margin-right: 15px"></i>
          <el-dropdown-menu slot="dropdown">
            <el-dropdown-item>查看</el-dropdown-item>
            <el-dropdown-item>新增</el-dropdown-item>
            <el-dropdown-item>删除</el-dropdown-item>
          </el-dropdown-menu>
        </el-dropdown>
        <!-- 取出在store中index.js设置的全局属性admin{username:''} -->
        <!-- <span>{{this.$store.state.admin.username}}</span> -->
        <!-- 因为才计算属性计算了,所以通过getters也可以去到 -->
        <span>{{ this.$store.getters.getAdmin.username }}</span>
      </el-header>

      <el-main>
        <router-view></router-view>
      </el-main>
    </el-container>
  </el-container>
</template>
<script>
export default {
  mounted() {
    //定时刷新token
    setInterval(this.refreshToken, 1000*60*60*24);
  },

  //import引入的组件需要注入到对象中才能使用
  components: {},
  props: {},
  data() {
    //这里存放数据
    return {};
  },

  methods: {
    refreshToken() {
      //发送axios请求,获取新的token
      this.axios({
        url:"http://localhost:8080/admin/refreshToken",
      }).then((res) => {
        // 后端生成新的token,响应回来再存起来
        window.localStorage.setItem("token",res.data.data.token)
      })
    },
  },

  //生命周期 创建之前
  beforeCreate() {
 //从本地存储拿到登录成功之后写回来的token,设置全局的headers携带token
    this.axios.defaults.headers.common["token"] = `${localStorage.getItem("token")}`;
  }
};
</script>
<style scoped>
/* @import url(); 引入公共css类 */
.el-header {
  background-color: #b3c0d1;
  color: #333;
  line-height: 60px;
}
.el-aside {
  color: #333;
}
</style>

注意:当用户登录成功后,后端响应一个token,前端接受token将token存到本地在 beforeCreate()里设置了全局的token,这样就不用每次请求都设置token请求头了。

 13.2.4条件和分页查询页面及代码

在src下的pages文件夹下面创建AdminList.vue

根据需要自行设置每页显示条数,只需更改data里面的pageSize属性。

<template>
  <div class="">
    <el-form :inline="true" :model="formInline" class="demo-form-inline">
      <el-form-item label="账号名称">
        <el-input v-model="formInline.username" placeholder="用户名"></el-input>
      </el-form-item>
      <el-form-item label="类型">
        <el-select v-model="type" clearable placeholder="请选择">
          <el-option
            v-for="item in types"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          >
          </el-option>
        </el-select>
      </el-form-item>
      <el-form-item label="状态">
        <el-select v-model="flag" clearable placeholder="请选择">
          <el-option
            v-for="item in flags"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          >
          </el-option>
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="success" @click="getAdmins()">查询</el-button>
        <el-button type="success" @click="handleClickAdd()">新增</el-button>
      </el-form-item>
    </el-form>
    <el-table :data="admins" stripe style="width: 100%; height: 100%">
      <el-table-column prop="id" label="编号" width="110"></el-table-column>
      <el-table-column
        prop="username"
        label="用户名"
        width="140"
      ></el-table-column>
      <el-table-column prop="password" label="密码" width="140">
      </el-table-column>
      <el-table-column
        prop="flag"
        label="状态"
        width="112"
        :formatter="formatterFlag"
      >
      </el-table-column>
      <el-table-column prop="type" label="类型" width="140"> </el-table-column>
      <el-table-column prop="createTime" label="创建时间" width="150">
      </el-table-column>
      <el-table-column prop="updateTime" label="更新时间" width="150">
      </el-table-column>
      <el-table-column prop="picture" label="头像" width="150">
        <!-- 图片的显示 -->
        <template slot-scope="scope">
          <el-image
            style="width: 100px; height: 100px"
            :src="scope.row.picture"
            @click="previewPic(scope.row.picture)"
            :preview-src-list="srcList"
          >
          </el-image>
        </template>
      </el-table-column>
      <el-table-column fixed="right" label="操作" width="200">
        <template slot-scope="scope">
          <!-- 查看详情 -->
          <el-button
            @click="handleClickInfo(scope.row)"
            icon="el-icon-search"
            circle
          ></el-button>
          <!-- 编辑 -->
          <el-button
            @click="handleClickEdit(scope.row)"
            type="primary"
            icon="el-icon-edit"
            circle
          ></el-button>
          <!-- 删除 -->
          <el-button
            @click="handleClickDelete(scope.row)"
            type="danger"
            icon="el-icon-delete"
            circle
          ></el-button>
        </template>
      </el-table-column>
    </el-table>

    <el-pagination
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
      :current-page="currentPage"
      :page-sizes="[10, 15, 20, 25, 30]"
      :page-size="pageSize"
      :total="total"
      layout="total,sizes,prev,pager,next,jumper"
    >
    </el-pagination>
    <!-- 编辑和添加对话框路由显示 -->
    <router-view></router-view>
  </div>
</template>
<script>
export default {
  //import引入的组件需要注入到对象中才能使用
  components: {},
  props: {},
  data() {
    //这里存放数据,渲染表格
    return {
      srcList: [
        "https://fuss10.elemecdn.com/8/27/f01c15bb73e1ef3793e64e6b7bbccjpeg.jpeg",
      ], // 图片预览集合,里面放链接
      formInline: {
        username: "", //条件查询用户名
        // type: "", // 查询类型
        // flag: "", // 查询状态
      },
      currentPage: 1, // 默认第一页
      pageSize: 3, // 默认每页显示10条
      total: 0, // 总条目数,默认0
      admins: [],

      //下拉框方式搜索账号类型
      types: [
        {
          value: "",
          label: "全部",
        },
        {
          value: "管理员",
          label: "管理员",
        },
        {
          value: "用户",
          label: "用户",
        },
      ],
      type: "",

      //下拉框方式搜索账号状态
      flags: [
        {
          value: "",
          label: "全部",
        },
        {
          value: "1",
          label: "有效",
        },
        {
          value: "2",
          label: "无效",
        },
        {
          value: "3",
          label: "冻结",
        },
      ],
      flag: "",
    };
  },
  //监听属性 类似于data概念
  computed: {},
  //监控data中的数据变化
  watch: {},
  //方法集合
  methods: {
    // 点击图片预览,
    previewPic(url) {
      this.srcList = []; // 清空srcList数组

      //push():在末尾追加一个或多个元素返回新的长度
      //pop():删除并返回数组的最后一个元素
      //shift():删除并返回数组的第一个元素
      this.srcList.push(url);
    },
    //查询用户信息
    getAdmins() {
      console.log(this.flag);
      console.log(this.type);
      this.axios({
        methods: "get",
        url: "http://localhost:8080/admin/list",
        params: {
          page: this.currentPage, // 获取当前页码传给后台
          limit: this.pageSize, // 获取每页条数传给后台
          username: this.formInline.username, // 搜索关键字
          // type: this.formInline.type, // 搜索关键字
          // flag: this.formInline.flag, // 搜索关键字
          type: this.type, // 下拉框方式选择搜索类型
          flag: this.flag, // 下拉框方式选择搜索状态
        },
      })
        .then((res) => {
          if (res.data.code == 0) {
            // 将查询出来的admin集合和总条数赋值给data里面的admins
            this.admins = res.data.data;
            this.total = res.data.count;
            // console.log("查询用户所有信息");
            // console.log(res.data.data);
          } else {
            console.log("用户信息查询失败了!!!");
          }
        })
        .catch(function (error) {
          console.log(error);
        });
    },

    //选择每页条数时触发
    handleSizeChange(val) {
      //每页的size条数变化的时候触发
      console.log(val);
      this.pageSize = val; // 更新pageSize
      this.currentPage = 1;

      //每次改变都再次发送axios方法,请求后台
      this.getAdmins();
    },

    // 点击上/下页时触发
    handleCurrentChange(val) {
      //当前页面变化的时候触发
      this.currentPage = val; // 更新currentPage

      //每次改变都再次发送axios方法,请求后台
      this.getAdmins();
    },

    //数字类型与字符类型转换
    formatterFlag(rows) {
      // 1表示有效  2表示无效  3表示冻结
      return rows.flag == 1 ? "有效" : 2 ? "无效" : "冻结";
    },

    //增加用户
    handleClickAdd() {
      this.$router.push({
        path: "/home/adminAdd",
      });

      //每次改变都再次发送axios方法,请求后台
      this.getAdmins();
    },

    //查看详情
    handleClickInfo(row) {
      this.$router.push({
        path: "/home/adminDetail",
        query: {
          id: row.id,
        },
      });
    },

    //编辑
    handleClickEdit(row) {
      console.log(row);
      //打开编辑页面 前端的页面 跳转的时候可以携带参数
      this.$router.push({
        path: "/home/adminEdit",
        query: {
          id: row.id, // 参数id
        },
      });

      //每次改变都再次发送axios方法,请求后台
      this.getAdmins();
    },

    //删除
    handleClickDelete(row) {
      this.$confirm("此操作将永久删除该文件, 是否继续?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(() => {
          //点击确定删除进入此处
          this.axios({
            methods: "post",
            url: "http://localhost:8080/admin/delete",
            params: {
              id: row.id,
            },
          }).then((res) => {
            if (res.data.code == 200) {
              this.$message({
                type: "success",
                message: "删除成功!",
              });
              this.getAdmins();
            } else {
              this.$message({
                type: "error",
                message: "删除失败!",
              });
            }
          });
        })
        .catch(() => {
          this.$message({
            type: "info",
            message: "已取消删除",
          });
        });
    },
  },

  //生命周期 - 创建完成(可以访问当前this实例)
  created() {},
  //生命周期 - 挂载完成(可以访问DOM元素)
  mounted() {
    //页面加载完成,查询列表信息
    this.getAdmins();
  },
};
</script>
<style scoped>
/* @import url(); 引入公共css类 */
</style>

13.2.5单个文件上传代码

 在src文件夹下创建components文件夹,如果前面policy.js配置文件时已经创建就直接在component是下创建singleUpload.vue.

<template>
  <div class=''>
    <!--:file-list="fileList"带这个参数图片上就不显示文件名,不能点击查看了 -->
    <el-upload 
    action="https://ymk1.oss-cn-hangzhou.aliyuncs.com"
    :data="dataObj"
    list-type="picture"
    :multiple="false"
    :show-file-list="showFileList"
    :before-upload="beforeUpload"
    :on-remove="handleRemove"
    :on-success="handleUploadSuccess"
    :on-preview="handlePreview"
    :on-progress="uploadProgress"
    >
      <el-button size="small" type="primary">点击上传</el-button>
      <div slot="tip" class="el-upload_tip">
        只能上传jpg/png文件,且不超过10MB
      </div>
    </el-upload>
    <el-dialog :visible.sync="dialogVisible">
        <!-- 计算属性中fileList第一个元素的url -->
    <img width="100%" :src="fileList[0].url" alt=""/>
    </el-dialog>
  </div>
</template>

<script>
import { policy } from '@/components/upload/policy';
import { getUUID } from '@/utils';
  export default {
    //import引入的组件需要注入到对象中才能使用
    components: {},
    props: {
        //value是AdminAdd.vue中的singleUpload标签里的v-model传过来的
        // v-model是双向绑定的
        value: String, 
    },
    data() {
      //这里存放数据
      return {
        dataObj:{
            policy: "",
            signature: "",
            key: "",
            ossaccessKeyId: "",
            dir: "",
            host: "",
        },
        dialogVisible: false,
      }
    },
    //监听属性 类似于data概念
    computed: {
        
        imageUrl(){
            // value是AdminAdd.vue中的singleUpload标签里的v-model传过来的
            return this.value; 
        },

        //图片名字
        imageName(){
            if(this.value != null && this.value !== ""){
                return this.value.substr(this.value.lastIndexOf("/") + 1);
            } else {
                return null;
            }
        },

        //图片列表 返回是个数组
        fileList(){
            return[
                {
                    // name: this.imageName,
                    url: this.imageUrl,
                },
            ];
        },

        showFileList: {
            get: function (){
                return(
                    this.value !== null && this.value !== "" && this.value !== undefined
                );
            },
            set: function (newValue){},
        },
    },
    //监控data中的数据变化
    watch: {},
    //方法集合
    methods: {

        emitInput(val){
            console.log("上传之后的图片地址:"+val)
            this.$emit("input",val); 
        },

        //移除图片
        handleRemove(file,fileList){
            this.emitInput("");
        },

        //点击文件列表中已上传的文件时的钩子
        handlePreview(file){
            this.dialogVisible = true;
        },

        // 上传文件之前触发
        beforeUpload(file){
            console.log("......")

            let _self = this;
            return new Promise((resolve,reject) => {
                policy(_self) //调的是
                .then((res) => {
                    // policy 策略
                    _self.dataObj.policy = res.data.policy;
                    // signature 签名
                    _self.dataObj.signature = res.data.signature;
                    // oss acess-key 
                    _self.dataObj.ossaccessKeyId = res.data.accessid;
                    // 上传之后的文件 路径+文件名   上传日期/uuid + 文件名
                    _self.dataObj.key = res.data.dir + getUUID() + "_${filename}";
                    // dir 是上传到oss存放的路径
                    _self.dataObj.dir = res.data.dir;
                    // host:http://localhost:8080/oss/policy
                    _self.dataObj.host = res.data.host;
                    resolve(true);
                })
                .catch((err) => {
                    reject(false);
                });
            })
        },

        //上传成功触发 拿到上传的文件的路径,然后回显
        handleUploadSuccess(res,file) {
            console.log("上传成功...");
            console.log(file.name)
            this.showFileList = true;
            this.fileList.pop();
            this.fileList.push({
                name: "success",
                url:
                  this.dataObj.host+
                  "/" +
                  this.dataObj.key.replace("${filename}",file.name),
            });
            // 调用methods中的emitInput(val)方法,将上传后的地址赋值
            this.emitInput(this.fileList[0].url);
        },
    },
  }
</script>
<style scoped>
 /* @import url(); 引入公共css类 */

</style>

13.2.6添加用户页面及代码

在src下的pages文件夹下面创建AdminAdd.vue

该页面是通过点击新增寻找/home/adminAdd路径的路由,可以看到改页面弹出在home页面之上,这是因为在home的路由配置通过children[{}]里面设置了嵌套路由

<template>
  <div class="">
    <!-- 添加用户对话框 -->
    <el-dialog
      title="添加用户"
      :close-on-click-modal="false"
      :visible.sync="addDialogVisible"
      width="40%"
    >
      <!-- 内容主题 带有变单验证-->
      <el-form
        :model="admin"
        :rules="addAdminFormRules"
        ref="addAdminForm"
        label-position="left"
        label-width="80px"
      >
        <el-form-item label="用户名" prop="username">
          <el-input v-model="admin.username"></el-input>
        </el-form-item>
        <el-form-item label="密码" prop="password">
          <el-input v-model="admin.password"></el-input>
        </el-form-item>
        <el-form-item label="状态" prop="flag">
          <el-select v-model="admin.flag" clearable placeholder="请选择">
            <el-option
              v-for="item in flags"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            >
            </el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="类型" prop="type">
          <el-select v-model="admin.type" clearable placeholder="请选择">
            <el-option
              v-for="item in types"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            >
            </el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="头像" prop="picture">
          <!-- 调用singleUpload.vue页面 -->
          <!-- v-model将上传成功后的地址传给admin.picture -->
          <single-upload v-model="admin.picture"></single-upload>
        </el-form-item>
        <el-form-item label="操作">
          <el-button type="primary" @click="submitForm('addAdminForm')"
            >提交</el-button
          >
          <el-button @click="cancel()">取消</el-button>
        </el-form-item>
      </el-form>
    </el-dialog>
  </div>
</template>

<script>
import SingleUpload from "../components/upload/singleUpload.vue";
export default {
  //import引入的组件需要注入到对象中才能使用
  components: { SingleUpload },
  props: {},
  data() {
    //这里存放数据
    return {
      //添加用户的表单数据
      admin: {},
      imageUrl: "", //图片地址
      //控制添加用户对话框的显示与隐藏
      addDialogVisible: true,

      //添加表单的验证规则对象
      addAdminFormRules: {
        username: [
          { required: true, message: "请输入用户名", trigger: "blur" },
          {
            min: 1,
            max: 10,
            message: "用户名长度在1~10个字符",
            trigger: "blur",
          },
        ],
        password: [
          { required: true, message: "请输入密码", trigger: "blur" },
          { min: 4, max: 15, message: "密码长度在4~15个字符", trigger: "blur" },
        ],
      },

      //下拉框方式添加时选择账号状态
      flags: [
        {
          value: "1",
          label: "有效",
        },
        {
          value: "2",
          label: "无效",
        },
        {
          value: "3",
          label: "冻结",
        },
      ],
      flag: "",

      //下拉框添加时选择账号类型
      types: [
        {
          value: "管理员",
          label: "管理员",
        },
        {
          value: "用户",
          label: "用户",
        },
      ],
      type: "",
    };
  },
  //监听属性 类似于data概念
  computed: {},
  //监控data中的数据变化
  watch: {},
  //方法集合
  methods: {
    //formName可用来校验表单信息
    submitForm(formName) {
      this.$refs[formName].validate((valid) => {
        //当表单验证通过之后才能进入if语句中
        if (valid) {
          this.axios({
            method: "post",
            url: "http://localhost:8080/admin/add",
            data: this.admin,
          })
            .then((res) => {
              debugger;
              console.log("添加用户信息响应数据");
              console.log(res);
              debugger;
              if (res.data.code == 200) {
                // 添加成功给出提示
                this.$message(
                  {
                    type: "success",
                    message: "添加成功!",
                  },
                  1000
                );

                // 通过路由跳转页面
                this.$router.push({
                  path: "/home/adminList",
                });
              } else if (res.data.code == 444) {
                //添加失败给出提示
                this.$message(
                  {
                    type: "error",
                    message: "添加失败",
                  },
                  1000
                );
              }

              // 通过路由跳转页面
              this.$router.push({
                path: "/home/adminList",
              });
            })
            .catch((error) => {
              console.log(error);
            });
        }
      });
    },

    //点击取消路由到adminList页面
    cancel() {
      this.$router.push({
        path: "/home/adminList",
      });
    },
  },
};
</script>
<style scoped>
</style>

13.2.7删除动作及代码

该功能使用的弹出框代码在Home.vue中。

methods中handleClickDelete()方法里.

13.2.8编辑页面及代码

 在src下的pages文件夹下面创建AdminEdit.vue

<!--  -->
<template>
  <div class="">
     <!-- 添加用户对话框 -->
     <el-dialog title="修改用户" :close-on-click-modal="false"  :visible.sync="editDialogVisible" width="40%">
      <!-- 内容主题 带有变单验证-->
    <el-form
      ref="editAdminForm"
      label-position="left"
      label-width="80px"
      :model="admin"
    >
      <el-form-item label="用户名">
        <el-input v-model="admin.username"></el-input>
      </el-form-item>
      <el-form-item label="密码">
        <el-input v-model="admin.password"></el-input>
      </el-form-item>
      <el-form-item label="状态">
        <!-- <el-input v-model="admin.flag"></el-input> -->
        <el-select v-model="admin.flag" clearable placeholder="请选择">
          <el-option
            v-for="item in flags"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          >
          </el-option>
          </el-select>
      </el-form-item>
      <el-form-item label="类型">
        <!-- <el-input v-model="admin.type"></el-input> -->
        <el-select v-model="admin.type" clearable placeholder="请选择">
          <el-option
            v-for="item in types"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          >
          </el-option>
        </el-select>
      </el-form-item>
      <el-form-item label="头像">
         <!-- 调用singleUpload.vue页面 -->
          <!-- v-model将上传成功后的地址传给admin.picture -->
          <single-upload v-model="admin.picture"></single-upload>

      </el-form-item>
      <el-form-item label="操作">
        <el-button type="primary" @click="submitForm('editAdminForm')"
          >提交</el-button
        >
        <el-button @click="cancel()">取消</el-button>
      </el-form-item>
    </el-form>
  </el-dialog>
  </div>
</template>

<script>
// 这里需要引入singleUpload.vue组件,不然<single-upload>用不了
import SingleUpload from '../components/upload/singleUpload.vue';
export default {
  //import引入的组件需要注入到对象中才能使用
  components: {SingleUpload},
  props: {},
  data() {
    //这里存放数据
    return {
      admin: {},

      //控制修改用户对话框的显示与隐藏
      editDialogVisible: true,

      //下拉框方式回显账号状态
      flags: [
        {
          value: "1",
          label: "有效",
        },
        {
          value: "2",
          label: "无效",
        },
        {
          value: "3",
          label: "冻结",
        },
      ],
      flag: "",
      //下拉框回显账号类型
      types: [
        {
          value: "管理员",
          label: "管理员",
        },
        {
          value: "用户",
          label: "用户",
        },
      ],
      type: "",

    };
  },
  //监听属性 类似于data概念
  computed: {},
  //监控data中的数据变化
  watch: {},
  //方法集合
  methods: {
    //通过用户id查询信息回显
    getAdminById(id) {
      this.axios({
        methods: "get",
        url: "http://localhost:8080/admin/adminDetail",
        params: {
          id: id,
        },
      })
        .then((res) => {
          console.log("通过id查用户信息回显");
          console.log(res);
          //要看懂 后台响应回来的数据格式,拿到咱们要取得的数据
          if (res.data.code == 200) {
            this.admin = res.data.data.admin;
          }
        })
        .catch((error) => {
          console.log(error);
        });
    },

    //提交表单信息触发
    submitForm(formName) {
      this.$refs[formName].validate((valid) => {
        if (valid) {
          //发送axios请求
          this.axios.post('http://localhost:8080/admin/update',this.admin)
            .then((res) => {
              console.log("更新提交后台响应的数据");
              console.log(res);
              //要看懂 后台响应回来的数据格式,拿到咱们要取得的数据
              if (res.data.code == 200) {

                // 修改成功给出提示
                this.$message({
                type: "success",
                message: "修改成功!",
              },1000);
                
                // 通过路由跳转页面
                this.$router.push({
                  path:'/home/adminList'
                })

              } else if(res.data.code == 444){
                this.$message({
                type: "error",
                message: "修改失败!",
              },1000);

              // 通过路由跳转页面
              this.$router.push({
                  path:'/home/adminList'
                })

              }
            })
            .catch((error) => {
              console.log(error);
            });
        } else {
          console.log("error submit!");
          return false;
        }
      });
    },
    cancel() {
      this.$router.push({
        path: "/home/adminList",
      });
    },

  },
  //生命周期 - 挂载完成(可以访问DOM元素)
  mounted() {
    //获取 路由跳转携带的参数
    let id = this.$route.query.id;

    //获取用户详情
    this.getAdminById(id);
  },
};
</script>
<style scoped>
/* @import url(); 引入公共css类 */
</style>

13.3后端相关配置代码

目录 

13.3.1pom.xml文件

<!--—————web开发环境,springMvc相关的依赖 内置tomcat—————-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--—————自带热部署插件—————-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <!--—————配置类相关的依赖—————-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

        <!--—————lombok可以自动生成 get set—————-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.20</version>
            <optional>true</optional>
        </dependency>

        <!--—————测试—————-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!--—————mysql依赖—————-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!--————— mybatis-plus代码生产器 —————-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.3.2</version>
        </dependency>

        <!--—————模板 代码生成器使用模板进行生产—————-->
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity</artifactId>
            <version>1.7</version>
        </dependency>

        <!--—————mybatis-plus 扩展插件 比如 分页插件依赖—————-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-extension</artifactId>
            <version>3.4.2</version>
        </dependency>

        <!--—————mysql依赖—————-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!--—————fastJson依赖—————-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.56</version>
        </dependency>

        <!--————— mybatis-plus插件依赖 —————-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>

        <!--————— mybatis-plus代码生产器 —————-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.3.2</version>
        </dependency>

        <!--—————knife4j 依赖—————-->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <version>2.0.7</version>
        </dependency>

        <!--—————aop依赖—————-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <!--——————新版本整合oss文件上传——————-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>aliyun-oss-spring-boot-starter</artifactId>
            <version>1.0.0</version>
        </dependency>

        <!--————————整合jwt token————————-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.0</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.7.0</version>
        </dependency>

13.3.2application.yml文件


# 服务端口号
server:
  port: 8080


spring:
  datasource: # 数据库配置
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/haiweiaicloud?serverTimezone=GMT%2B8
    username: root
    password: 123456

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 查看sql输出日志

  global-config:
    db-config:
      logic-delete-value: 1  # 逻辑删除   1 代表已被逻辑删除
      logic-not-delete-value: 0 # 逻辑删除   0 代表未被逻辑删除
      id-type: auto  # 整个项目中设置主键自增
  mapper-locations: classpath:mapper/*.xml  # 映射mapper中xml文件

#OSS文件上传配置
alibaba:
  cloud:
    access-key: LTAI5tPsayZbQq9DNzGztFPL
    secret-key: bDWiypa4C2qrUmT8w2zfDsCiJ5ZbUC
    oss:
      endpoint: oss-cn-hangzhou.aliyuncs.com
      bucket: ymk1

13.3.3拦截器及跨域配置

全局拦截器,此代码在interceptor包下,类名MyInterceptor

作用:

        进行了前置拦截的相关操作,根据token判断用户登录

package com.ymk.hotelsystem.interceptor;

import com.alibaba.fastjson.JSON;
import com.ymk.hotelsystem.annotation.UnInterception;
import com.ymk.hotelsystem.common.CheckResult;
import com.ymk.hotelsystem.common.R;
import com.ymk.hotelsystem.common.SystemConstant;
import com.ymk.hotelsystem.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;

/**
 * @Desc 拦截器
 */
@Slf4j
public class MyInterceptor implements HandlerInterceptor {
    /**
     * 前置拦截
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("拦截器前置拦截"); // 可以在此方法中解决跨域问题

        //当含有自定义请求头时必须进行域减请求
        if ("OPTIONS".equals(request.getMethod())) {//处理预检请求
            System.out.println("preflight 请求");
            return true;
        }


        System.out.println(request.getRequestURI()); // 获取请求的路径
        /**
         * 判断是否登录
         */
        // 方法处理器
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        // 为防止类转换异常可以在这做个判断
        if (handler instanceof HandlerMethod) {

            Method method = handlerMethod.getMethod();
            String methodName = method.getName();
            log.info("====拦截到了方法:{},在该方法执行之前执行====", methodName);

            // 通过方法,可以获取该方法上的自定义注解,然后通过注解来判断该方法是否要被拦截
            // @UnInterception 是自定义注解
            UnInterception unInterception = method.getAnnotation(UnInterception.class);
            if (null != unInterception) {
                return true;
            }

            //判断用户有没有登录,一般登录之后的用户都有一个对应的token
            String token = request.getHeader("token");
            System.out.println("获取请求头token:" + token);
            if (null == token || "".equals(token)) {
                log.info("用户未登录,没有权限执行...请登录");

                //token为空时将信息响应到前端
                R r = R.error().message(SystemConstant.JWT_ERRCODE_NULL + ":用户未登录");
                print(response, r);
                return false;
            } else {
                // 校验token 的逻辑
                CheckResult checkResult = JwtUtils.validateJWT(token);

                if (checkResult.isSuccess()) {
                    //token令牌校验通过
                    return true;
                } else {
                    // 解析失败时将分情况通过switch响应前端
                    switch (checkResult.getErrCode()) {
                        case SystemConstant.JWT_ERRCODE_FAIL: {
                            System.out.println("签名校验不通过");
                            R r = R.error().message(SystemConstant.JWT_ERRCODE_FAIL + ":签名校验不通过");
                            print(response, r);
                            break;
                        }
                        case SystemConstant.JWT_ERRCODE_EXPIRE: {
                            System.out.println("签名已过期");
                            R r = R.error().message(SystemConstant.JWT_ERRCODE_EXPIRE + ":签名已过期");
                            print(response, r);
                            break;
                        }

                    }
                    // token校验失败返回false 拦截
                    return false;
                }
            }
        }
        // 返回true才会继续执行,返回false则取消了当前请求
        return true;
    }

    /**
     * 当token解析失败时向前端响应
     *
     * @param response
     * @param r
     */
    public void print(HttpServletResponse response, R r) {
        // 响应json数据
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        PrintWriter writer = null;
        try {
            writer = response.getWriter();
            // 把java 对象转为 json串
            String jsonString = JSON.toJSONString(r);
            // 响应回去的数据是json 格式
            writer.print(jsonString);
            writer.flush();
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 后置拦截
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("执行完方法后执行(Controller方法调用之后),但是此时还没进行视图渲染");
    }

    /**
     * 最终拦截
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("整个请求都处理完毕,DispatcherServlet也渲染了对应的视图了,此时可以做一些清理工作");
    }
}

下面代码在config包下,类名MyMvcConfig

作用: 

        解决跨域,设置不需要拦截的路径

package com.ymk.hotelsystem.config;

import com.ymk.hotelsystem.interceptor.MyInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @Desc MVC相关的一个配置
 */
@Configuration // 声明配置类
public class MyMvcConfig implements WebMvcConfigurer {
    /**
     * 跨域解决
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // registry 解决跨域问题
        registry.addMapping("/**")
                .allowedOrigins("*") // 允许任何的域
                .allowCredentials(true) // 允许携带cookie
                .allowedMethods("GET","POST","PUT","DELETE","HEAD","PATCH","OPTIONS","TRACE","CONNECT") // 允许的请求方法
                .allowedHeaders("*"); // 允许头

    }

    /**
     * 配置拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MyInterceptor())
                //访问doc.html时有关路径不拦截
                .excludePathPatterns("/webjars/**","/swagger-resources/**","/doc.html") // 不用拦截的路径
                .addPathPatterns("/**"); // 拦截所有路径
    }
}

下面代码在annotation包下,注解名UnInterception,该自定义注解是用来指定某个方法不需要被拦截。只能写在方法上

package com.ymk.hotelsystem.annotation;

/**
 * @Desc 自定义注解
 */
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 该注解用来指定某个方法不用拦截
 */
@Target(ElementType.METHOD) // 只能写在方法上
@Retention(RetentionPolicy.RUNTIME)
public @interface UnInterception {
}

13.3.4全局异常和自定义异常

下面代码在exception包下,类名GlobalExceptionHandler

作用:

        全局异常处理

package com.ymk.hotelsystem.exception;

import com.ymk.hotelsystem.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

/**
 * @Desc 全局异常处理器
 */
@ControllerAdvice
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
    /**
     * 拦截业务异常,返回业务异常信息
     * @param ex
     * @return
     */
    @ExceptionHandler(BusinessErrorException.class)
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    public R handleBusinessError(BusinessErrorException ex){
        String code = ex.getCode();
        String message = ex.getMessage();
        return R.error().code(Integer.parseInt(code)).message(message);
    }

    /**
     * 空指针异常
     * @param ex NullPointerException
     * @return
     */
    @ExceptionHandler(NullPointerException.class)
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    public R handleTypeMismatchException(NullPointerException ex) {
        log.error("空指针异常,{}", ex.getMessage());
        return R.error().message("空指针异常了");
    }

    /**
     * 缺少请求参数异常
     * @param ex MissingServletRequestParameterException
     * @return
     */
    @ExceptionHandler(MissingServletRequestParameterException.class)
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    public R handleHttpMessageNotReadableException(MissingServletRequestParameterException ex){
        log.error("缺少请求参数,{}", ex.getMessage());
        return R.error().message("缺少必要的请求参数");
    }

    /**
     * 系统异常 预期以外异常
     * @param ex
     * @return
     * 项目中,我们一般都会比较详细的去拦截一些常见异常,拦截Exception 虽然可以一劳永逸,
     * 但是不利于我们去排查或者定位问题。实际项目中,可以把拦截 Exception 异常写在 GlobalExceptionHandler
     * 最下面,如果都没有找到,最后再拦截一下 Exception 异常,保证输出信息友好。
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    public R handleUnexpectedServer(Exception ex){
        log.error("系统异常:", ex);
        return R.error().message("系统发生异常,请联系管理员");
    }
}

 下面代码在common包下,枚举类名BusinessMsgEnum

作用:        

        自定义异常状态码及提示信息

package com.ymk.hotelsystem.common;

/**
 * @Desc
 */

/**
 * 业务异常提示信息枚举类
 */
public enum BusinessMsgEnum {
    /** 参数异常 */
    PARMETER_EXCEPTION("102","参数异常"),
    /** 等待超时 */
    SERVICE_TIME_OUT("103","服务调用超时"),
    /** 参数过大 */
    PARMETER_BIG_EXCEPTION("102","输入的图片数量不能超过50张"),
    /** 500 :一劳永逸的提示也可以在这定义 */
    UNEXPECTED_EXCEPTION("500","异常发生异常,请联系管理员!");

    // 还可以定义更多的也无异常
    /**
     * 消息码
     */
    private String code;
    /**
     * 消息内容
     */
    private String msg;

    private BusinessMsgEnum(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    public String getCode() {
        return code;
    }
    public void setCode(String code) {
        this.code = code;
    }
    public String getMsg() {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
}

下面代码在exception包下,类名BusinessErrorException

作用:

        自定义异常

package com.ymk.hotelsystem.exception;

import com.ymk.hotelsystem.common.BusinessMsgEnum;

/**
 * @Desc 自定义异常
 */
public class BusinessErrorException extends RuntimeException{
    /**
     * 异常码
     */
    private String code;
    /**
     * 异常提示信息
     */
    private String message;

    public BusinessErrorException(BusinessMsgEnum businessMsgEnum){
        this.code = businessMsgEnum.getCode();
        this.message = businessMsgEnum.getMsg();
    }
    public String getCode() {
        return code;
    }
    public void setCode(String code) {
        this.code = code;
    }
    @Override
    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }
}

13.3.5JWT生成及Token认证

下面代码在common包下,类名SystemConstant

作用:

        JWTUtil工具类解析token设置的状态码,和JWT相关属性

package com.ymk.hotelsystem.common;

/**
 * @Desc  jwtUtil工具类用到的
 */
public class SystemConstant {
    /**
     * token
     */
    public static final int JWT_ERRCODE_NULL = 4000;  // token不存在
    public static final int JWT_ERRCODE_EXPIRE = 4001; // token已过期
    public static final int JWT_ERRCODE_FAIL= 4002;    //验证不通过

    /**
     * JWT
     */
    public static final String JWT_SECERT = "8677df7fc3a34e26a61c034d5ec8245d";
    public static final long JWT_TTL = 60 * 60 * 1000;
}

 下面代码在common包下,类名CheckResult

作用:

        用来验证JWT

package com.ymk.hotelsystem.common;

import io.jsonwebtoken.Claims;

/**
 * @Desc 验证JWT用到的
 */
public class CheckResult {

    private int errCode;

    private boolean success;

    private Claims claims;

    public int getErrCode(){
        return errCode;
    }

    public void setErrCode(int errCode){
        this.errCode = errCode;
    }

    public boolean isSuccess(){
        return success;
    }

    public void setSuccess(boolean success){
        this.success = success;
    }

    public Claims getClaims(){
        return claims;
    }

    public void setClaims(Claims claims){
        this.claims = claims;
    }
}

下面代码在utils包下,类名JwtUtils

作用:

        前端发送认证请求,比如用户名密码,认证成功后后台生成一个token 发送给前端,前端存储到localstorage,以后每次请求都带这个token发送到后台去验证

        

package com.ymk.hotelsystem.utils;

import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;
import com.ymk.hotelsystem.common.CheckResult;
import com.ymk.hotelsystem.common.SystemConstant;
import io.jsonwebtoken.*;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Date;

/**
 * @Desc 前端发送认证请求,比如用户名密码,认证荣国后后台生成一个token
 * 发送给前端,前端存储到localstorage,以后每次请求都带这个token发送到后台去验证
 */
public class JwtUtils {

    /**
     * 签发JWT
     *
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     */
    public static String createJwt(String id, String subject, long ttlMillis) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        SecretKey secretKey = generalKey();
        JwtBuilder builder = Jwts.builder()
                .setId(id)
                // 主题 一般是用户名
                .setSubject(subject)
                // 签发者
                .setIssuer("ymk")
                // 签发日期
                .setIssuedAt(now)
                // 签发算法以及密钥
                .signWith(signatureAlgorithm, secretKey);

        if (ttlMillis >= 0) {
            // 当前时间+有效时间
            long expMillis = nowMillis + ttlMillis;
            // 过期时间
            Date expDate = new Date(expMillis);
            // 设置过期时间
            builder.setExpiration(expDate);
        }
        return builder.compact();
    }

    /**
     * 生成加密key
     *
     * @return
     */
    public static SecretKey generalKey() {
        byte[] encodeKey = Base64.decode(SystemConstant.JWT_SECERT);
        SecretKey key = new SecretKeySpec(encodeKey, 0, encodeKey.length, "AES");
        return key;
    }

    /**
     * 验证JWT
     * @param jwtStr
     * @return
     */
    public static CheckResult validateJWT(String jwtStr) {
        CheckResult checkResult = new CheckResult();
        Claims claims = null;
        try {
            claims = parseJwt(jwtStr);
            checkResult.setSuccess(true);
            checkResult.setClaims(claims);
        } catch (ExpiredJwtException e) {
            checkResult.setErrCode(SystemConstant.JWT_ERRCODE_EXPIRE);
            checkResult.setSuccess(false);
        } catch (SignatureException e) {
            checkResult.setErrCode(SystemConstant.JWT_ERRCODE_FAIL);
            checkResult.setSuccess(false);
        } catch (Exception e) {
            checkResult.setErrCode(SystemConstant.JWT_ERRCODE_FAIL);
            checkResult.setSuccess(false);
        }
        return checkResult;
    }

    /**
     * 解析JWT字符串
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJwt(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }


    public static void main(String[] args) {
        // 后端生成token
        String sc = createJwt("1", "jack", SystemConstant.JWT_TTL);
        System.out.println(sc);

        //后端验证token
        CheckResult checkResult = validateJWT(sc);
        System.out.println(checkResult.isSuccess());
        System.out.println(checkResult.getErrCode());
        Claims claims = checkResult.getClaims();
        System.out.println(claims);
        System.out.println(claims.getId());
        System.out.println(claims.getSubject());

        // 刷新token 重新生成token
        Claims claims2 = validateJWT(sc).getClaims();
        String sc2 = createJwt(claims2.getId(), claims2.getSubject(), SystemConstant.JWT_TTL);
        System.out.println(sc2);
    }
}

13.3.6后端响应前端封装的格式

下面代码在common包下,枚举类名ResultCodeEnum

作用:

        设置响应后端的一些操作是否成功和状态码以及信息

package com.ymk.hotelsystem.common;

import lombok.Getter;

@Getter // get方法
public enum ResultCodeEnum {
    //枚举值
    SUCCESS(true,"操作成功",200),
    UNKNOWN_REASON(false,"操作失败",999),
    BAD_SQL_GRAMMAR(false,"sql语法错误",520),
    ERROR(false,"操作失败",444);

    private Boolean success;
    private String message;
    private Integer code;

    ResultCodeEnum(Boolean success, String message, Integer code) {
        this.success = success;
        this.message = message;
        this.code = code;
    }
}

下面代码在common包下,类名R

作用:

        后端返回给前端的 一个封装的数据结构

package com.ymk.hotelsystem.common;

import lombok.Data;

import java.util.HashMap;
import java.util.Map;

/**
 * 后端返回给前端的 一个 封装的 数据结构
 */
@Data
public class R {
    private Integer code; // 响应的状态码

    private String message; // 响应的信息

    private Boolean success; // 是否成功

    //封装响应的数据
    private Map<Object,Object> data = new HashMap<>();

    public R() {}

    // 返回成功的结果
    public static R ok(){
        R r = new R();
        r.setSuccess(ResultCodeEnum.SUCCESS.getSuccess());
        r.setCode(ResultCodeEnum.SUCCESS.getCode());
        r.setMessage(ResultCodeEnum.SUCCESS.getMessage());
        return r;
    }
    // 返回失败的结果
    public static R error(){
        R r = new R();
        r.setSuccess(ResultCodeEnum.ERROR.getSuccess());
        r.setCode(ResultCodeEnum.ERROR.getCode());
        r.setMessage(ResultCodeEnum.ERROR.getMessage());
        return r;
    }

    public static R setResult(ResultCodeEnum resultCodeEnum){
        R r = new R();
        r.setSuccess(resultCodeEnum.getSuccess());
        r.setCode(resultCodeEnum.getCode());
        r.setMessage(resultCodeEnum.getMessage());
        return r;
    }

    //创建一个R对象去调success方法,然后把失败或成功的值传进去
    public R success(Boolean success){
        this.setSuccess(success);
        return this;
    }

    public R message(String message){
        this.setMessage(message);
        return this;
    }

    public R code(Integer code){
        this.setCode(code);
        return this;
    }

    public R data(Object key,Object value){
        this.data.put(key,value);
        return this;
    }

    public R data(Map<Object,Object> map){
        this.setData(map);
        return this;
    }
}

下面的代码在common包下,类名LayuiPageVo<T>

作用:

        用来封装Layui渲染表格时接受后台响应的信息

package com.ymk.hotelsystem.common;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

/**
 * @Desc
 */
@Data//set,get,toString方法
@AllArgsConstructor//有参构造
@NoArgsConstructor//无参构造
public class LayuiPageVo<T> {

    private Integer code; // 0 表示成功
    private String msg;
    private Long count;

    private List<T> data;
}

13.3.7AOP切面配置

下面代码在aop包下,类名LogAspectHandler

作用:

        通过JointPoint 对象来获取一个签名,然后利用签名可以获取请求的包名、方法名,也可以用来记录一些信息,比如获取请求的url和ip

package com.ymk.hotelsystem.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * @Desc
 */
@Aspect
@Component
@Slf4j
public class LogAspectHandler {
    /**
     * 定义一个切面,拦截com.ymk.controller包和子包下的所用方法
     */
    @Pointcut("execution(* com.ymk.hotelsystem.controller..*.*(..))")
    public void pointCut() {
    }

    @Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
    public void annotationCut() {
    }

    /**
     * 在上面定义的切面方法之前执行该方法
     * @param joinPoint jointPoint
     * JointPoint 对象很有用,可以用它来获取一个签名,然后利用签名可以获取请求的包名、方法名,
     * 包括参数(通过 `joinPoint.getArgs()` 获取)等等。
     */
    @Before("pointCut()") // 前置通知
    public void doBefore(JoinPoint joinPoint) {
        log.info("====doBefore方法进入了====");

        // 获取签名
        Signature signature = joinPoint.getSignature();
        // 获取切入的包名
        String declaringTypeName = signature.getDeclaringTypeName();
        // 获取即将执行的方法名
        String funcName = signature.getName();
        log.info("即将执行方法为: {},属于{}包", funcName, declaringTypeName);

        // 也可以用来记录一些信息,比如获取请求的url和ip
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // 获取请求url
        String url = request.getRequestURL().toString();
        // 获取请求ip
        String ip = request.getRemoteAddr();
        log.info("用户请求的url为:{},ip地址为:{}", url, ip);
    }
}


13.3.8Knife4j配置

下面代码在config包下,类名Knife4jConfiguration

作用:

        前端和后端的联系,通过访问localhost:8080/doc.html可展示在线的 API 接口文档,进行测试接口数据

package com.ymk.hotelsystem.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;

/**
 * @Desc Knife4j配置
 */
@Configuration // 声明配置类
@EnableSwagger2WebMvc
public class Knife4jConfiguration {
    @Bean(value = "defaultApi2")
    public Docket defaultApi2() {
        Docket docket=new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(new ApiInfoBuilder()
                        //.title("swagger-bootstrap-ui-demo RESTful APIs")
                        .description("# swagger-bootstrap-ui-demo RESTful APIs")
                        .termsOfServiceUrl("http://www.xx.com/")
                        .contact("xx@qq.com")
                        .version("1.0")
                        .build())
                //分组名称
                .groupName("2.X版本")
                .select()
                //这里指定Controller扫描包路径
                .apis(RequestHandlerSelectors.basePackage("com.ymk.demo22.controller"))
                .paths(PathSelectors.any())
                .build();
        return docket;
    }
}

13.3.9Jackson为空数据变空串

下面代码在config包下,类名JacksonConfig

作用:

        向前端响应json数据的时候将为空的数据会变成空串。

package com.ymk.hotelsystem.config;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;

import java.io.IOException;

/**
 * @Desc Jackson配置
 */
@Configuration //声明配置类
public class JacksonConfig {
    //此配置 向前端响应json数据的时候 为空的数据会变成空串
    @Bean
    @Primary // 简单的说 就是 当Spring 容器扫描到某个接口有多个bean时,如果某个bean上有@Primary 注解 则这个bean 会被优先选用
    @ConditionalOnMissingBean(ObjectMapper.class)
    public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder){
        ObjectMapper objectMapper = builder.createXmlMapper(false).build();
        objectMapper.getSerializerProvider().setNullValueSerializer(new JsonSerializer<Object>() {
            @Override
            public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
                jsonGenerator.writeString("");
            }
        });
        return objectMapper;
    }
}

13.3.10MybatisPlus

乐观锁、逻辑删除、分页插件

下面代码在config包下,类名MybatisPlusConfig

作用:

        配置一些插件,该类配置了分页插件、乐观锁插件、逻辑删除相关的bean

package com.ymk.hotelsystem.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;
import com.baomidou.mybatisplus.core.injector.ISqlInjector;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Desc 配置一些插件
 */
@Configuration
@MapperScan("com.ymk.hotelsystem.mapper")
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor optimisticLockerInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

        // 分页插件
        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
        interceptor.addInnerInterceptor(paginationInnerInterceptor); // 添加分页拦截器 --- 分页插件

        // 乐观锁插件
        OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor = new OptimisticLockerInnerInterceptor();
        interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor); // 添加乐观锁拦截器 --- 乐观锁插件

        return interceptor;
    }

    //逻辑删除相关的bean
    @Bean
    public ISqlInjector sqlInjector(){
        return new DefaultSqlInjector();
    }
}

下面代码在handler包下,类名MyMetaObjectHandler

作用:

        设置插入修改时的填充自动填充某些字段的值、添加逻辑删除的默认值逻辑上未删除的默认值。

package com.ymk.hotelsystem.handler;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;

/**
 * @Desc 自动填充 某些 字段的值
 */
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("start insert fill....");// 添加时填充
        this.setFieldValByName("createTime",new Date(),metaObject);
        this.setFieldValByName("updateTime",new Date(),metaObject);
//        this.setFieldValByName("createTime", LocalDate.now(),metaObject);
//        this.setFieldValByName("updateTime",LocalDate.now(),metaObject);

        // 添加 乐观锁的 默认值是1
        this.setFieldValByName("version",1,metaObject);

        // 0 表示逻辑上未删除的
        this.setFieldValByName("deleted", 0, metaObject);
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("start update fill....");// 修改时填充
        this.setFieldValByName("updateTime",new Date(),metaObject);
    }
}

13.4后端操作代码

13.4.1实体类pojo

下面代码在pojo包下,类名Admin

作用:

        对应数据库中的t_admin表,字段与Admin属性一一对应。

package com.ymk.hotelsystem.pojo;

import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

/**
 * @Desc
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("t_admin") //指定数据库中的表名
public class Admin {

    @TableId(type = IdType.AUTO) //使用数据库中的自增策略
    private Integer id;

    private String username;

    private String password;

    private Integer flag;

    private String type;

    private String picture;


    @JsonFormat(pattern="yyyy-MM-dd")
    @TableField(value = "create_time",fill = FieldFill.INSERT)
    private Date createTime;

    @JsonFormat(pattern="yyyy-MM-dd")
    @TableField(value = "update_time",fill = FieldFill.INSERT_UPDATE) // 添加和修改都会赋值
    private Date updateTime;

    @TableField(fill = FieldFill.INSERT)
    @Version
    private Integer Version;

    @TableLogic
    @TableField(fill = FieldFill.INSERT)
    private Integer deleted;

}

13.4.2controller

下面代码在controller包下,类名OssController

作用:

        因为不再需要后端去上传到OSS上了,现在交给了前端,前端上传oss请求,后端响应凭证,然后前端拿着凭证上传OSS。

package com.ymk.hotelsystem.controller;

import com.aliyun.oss.OSS;
import com.aliyun.oss.common.utils.BinaryUtil;
import com.aliyun.oss.model.MatchMode;
import com.aliyun.oss.model.PolicyConditions;
import com.ymk.hotelsystem.annotation.UnInterception;
import com.ymk.hotelsystem.common.R;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @Desc 前端上传oss请求,后端响应凭证
 */
@RestController
public class OssController {
    @Resource
    OSS ossClient;

    @Value("${alibaba.cloud.oss.endpoint}")
    private String endpoint;

    @Value("${alibaba.cloud.oss.bucket}")
    private String bucket;

    @Value("${alibaba.cloud.access-key}")
    private String accessId;

    @RequestMapping("/oss/policy")
    @UnInterception
    public R policy() {

        //host的格式为bucketname.endpoint
        String host = "https://" + bucket + "." + endpoint;
        //callbackUrl为上传回调服务器的URL,将下面的IP和Port配置为自己的真实信息
//    String callbackUrl = "http://88.88.88.8888";
        String format = new SimpleDateFormat("yyyy-MM-dd").format(new Date());

        //用户上传文件是指定的前缀
        String dir = format + "/";

        Map<Object,Object> respMap = null;
        try {
            long expireTime = 30;
            long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
            Date expiration = new Date(expireEndTime);
            PolicyConditions policyConds = new PolicyConditions();
            policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
            policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);

            String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
            byte[] binaryData = postPolicy.getBytes("utf-8");
            String encodedPolicy = BinaryUtil.toBase64String(binaryData);
            String postSignature = ossClient.calculatePostSignature(postPolicy);

            respMap = new LinkedHashMap<Object, Object>();
            respMap.put("accessid", accessId);
            respMap.put("policy", encodedPolicy);
            respMap.put("signature", postSignature);
            respMap.put("dir", dir);
            respMap.put("host", host);
            respMap.put("expire", String.valueOf(expireEndTime / 1000));
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
        return R.ok().data(respMap);
    }
}

下面代码在controller包下,类名AdminController

作用:

        接受前端用户请求

package com.ymk.hotelsystem.controller;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.ymk.hotelsystem.annotation.UnInterception;
import com.ymk.hotelsystem.common.CheckResult;
import com.ymk.hotelsystem.common.LayuiPageVo;
import com.ymk.hotelsystem.common.SystemConstant;
import com.ymk.hotelsystem.utils.JwtUtils;
import com.ymk.hotelsystem.utils.OssUtil;
import com.ymk.hotelsystem.common.R;
import com.ymk.hotelsystem.pojo.Admin;
import com.ymk.hotelsystem.service.AdminService;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

/**
 * @Author 闫梦坤
 * @Date 2022/10/16 23:43
 * @Desc
 */
@RestController
@Slf4j
@RequestMapping("/admin")
public class AdminController {
    @Autowired
    private AdminService adminService;

    /**
     * 管理员登录
     * @param username
     * @param password
     * @return
     */
    @UnInterception
    @GetMapping("/login")
    public R login(String username, String password){
        log.info("控制层接受账号密码:"+username+":"+password);
        Admin admin = adminService.findByLogin(username, password);

        Map<Object, Object> map = new HashMap<>();
        if (admin != null){
            //生成token发送给前端
            String token = JwtUtils.createJwt(String.valueOf(admin.getId()),admin.getUsername(), SystemConstant.JWT_TTL);
            map.put("currentAdmin",admin);
            map.put("token",token);
            return R.ok().data(map);
        } else {
            return R.error().message(SystemConstant.JWT_ERRCODE_NULL+"");
        }
    }

    /**
     * 分页加模糊查询
     * @param map
     * @return
     */
    @RequestMapping("/list")
    public LayuiPageVo<Admin> adminList(@RequestParam Map<String,Object> map){
        System.out.println(map);
        LayuiPageVo<Admin> adminLayuiPageVo = adminService.adminList(map);
        return  adminLayuiPageVo;
    }

    /**
     * 新增账号
     * @param admin
     * @return
     */
    @PostMapping("/add")
    public R adminAdd(@RequestBody Admin admin){
        log.info("添加用户接受的值:"+admin);
        Integer result = adminService.adminAdd(admin);
        log.info("增加受影响行数:"+result);
        if (result > 0){
            return R.ok().message("新增成功");
        } else {
            return R.error().message("系统繁忙,请您稍后尝试");
        }
    }

    @PostMapping("/update")
    public R adminEdit(@RequestBody Admin admin){
        log.info("修改用户接受的值:"+admin);
        Integer result = adminService.adminEdit(admin);
        if (result>0){
            return R.ok().message("修改成功");
        } else {
            return R.error().message("修改失败,请重试");
        }
    }

    /**
     * 删除用户
     * @param id
     * @return
     */
    @PostMapping("/delete")
    public R adminDelete(Integer id){
        System.out.println("删除账号的id:"+id);
        Integer result = adminService.adminDelete(id);
        if (result>0){
            return R.ok().message("删除成功");
        } else {
            return R.error().message("删除失败");
        }
    }


    /**
     * 通过id查询用户
     */
    @GetMapping("/adminDetail")
    public R findAdminById(Integer id){
        log.info("接受的查询id:"+id);
        Admin admin = adminService.findAdminById(id);
        Map<Object, Object> map = new HashMap<>();
        map.put("admin",admin);
        return R.ok().data(map);
    }

    /**
     * 重新生成token
     * 前端页面设置每一段时间刷新token,
     * 并向后端发送axios请求来获取新的token
     * @return
     */
    @RequestMapping("/refreshToken")
    public R refreshToken(HttpServletRequest request){
        String token = request.getHeader("token");
        System.out.println("刷新获得旧的token:"+token);
        CheckResult checkResult = JwtUtils.validateJWT(token);
        Claims claims = checkResult.getClaims();
        //生成一个新的token
        String newToken = JwtUtils.createJwt(claims.getId(), claims.getSubject(), SystemConstant.JWT_TTL);
        return R.ok().data("token",newToken);
    }
}

13.4.3service

AdminService接口

package com.ymk.hotelsystem.service;

import com.ymk.hotelsystem.common.LayuiPageVo;
import com.ymk.hotelsystem.pojo.Admin;

import java.util.Map;

/**
 * @Author 闫梦坤
 * @Date 2022/10/16 23:41
 * @Desc
 */
public interface AdminService {
    /**
     * 管理员登录
     * @param username
     * @param password
     * @return
     */
    Admin findByLogin(String username, String password);

    /**
     * 分页加条件查询
     * @param map
     * @return
     */
    LayuiPageVo<Admin> adminList(Map<String,Object> map);

    /**
     * 增加账号
     * @param admin
     * @return
     */
    Integer adminAdd(Admin admin);

    /**
     * 修改账号信息
     * @param admin
     * @return
     */
    Integer adminEdit(Admin admin);

    /**
     * 删除用户
     * @param id
     * @return
     */
    Integer adminDelete(Integer id);


    /**
     * 通过id查询用户
     * @param id
     * @return
     */
    Admin findAdminById(Integer id);
}

AdminServiceImpl实现类

package com.ymk.hotelsystem.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ymk.hotelsystem.common.LayuiPageVo;
import com.ymk.hotelsystem.mapper.AdminMapper;
import com.ymk.hotelsystem.pojo.Admin;
import com.ymk.hotelsystem.service.AdminService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Map;

/**
 * @Desc
 */
@Service("adminService")
public class AdminServiceImpl implements AdminService {

    @Autowired
    private AdminMapper adminMapper;

    /**
     * 管理员登录
     * @param username
     * @param password
     * @return
     */
    @Override
    public Admin findByLogin(String username, String password) {
        QueryWrapper<Admin> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username", username).eq("password", password);
        return adminMapper.selectOne(queryWrapper);
    }

    /**
     * 分页加条件查询
     * @param map
     * @return
     */
    @Override
    public LayuiPageVo<Admin> adminList(Map<String, Object> map) {
        //加上空字符串变成字符串再转成Integer类型
        Integer page = Integer.parseInt(map.get("page")+"");
        Integer limit = Integer.parseInt(map.get("limit")+"");

        Page<Admin> adminPage = new Page<>(page,limit);
        QueryWrapper<Admin> queryWrapper = new QueryWrapper<>();
        // 通过遍历的方式将map集合中不参加模糊查询的键值过滤,将模糊查询放到条件构造器中
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            if (!entry.getKey().equals("token") && !entry.getKey().equals("page") && !entry.getKey().equals("limit")){
                queryWrapper.like(entry.getKey(), entry.getValue());
            }
        }

        adminPage = adminMapper.selectPage(adminPage, queryWrapper);
        //把mybatis的分页数据封装到 layui 的分页数据结构中
        LayuiPageVo<Admin> layuiPageVo = new LayuiPageVo<>();
        layuiPageVo.setCode(0);// layui 默认0 是正确的状态码
        layuiPageVo.setMsg("分页列表数据"); //提示信息
        layuiPageVo.setCount(adminPage.getTotal()); // 总记录数
        layuiPageVo.setData(adminPage.getRecords()); // 分页的列表数据
        return layuiPageVo;
    }

    /**
     * 新增用户
     * @param admin
     * @return
     */
    @Override
    public Integer adminAdd(Admin admin) {
        int insert = adminMapper.insert(admin);
        return insert;
    }

    /**
     * 修改用户信息
     * @param admin
     * @return
     */
    @Override
    public Integer adminEdit(Admin admin) {
        UpdateWrapper<Admin> updateWrapper = new UpdateWrapper<>();
        updateWrapper.eq("id",admin.getId());
        int update = adminMapper.update(admin, updateWrapper);
        return update;
    }

    /**
     * 删除用户
     * @param id
     * @return
     */
    @Override
    public Integer adminDelete(Integer id) {
        int i = adminMapper.deleteById(id);
        return i;
    }


    /**
     * 通过id查询用户
     * @param id
     * @return
     */
    @Override
    public Admin findAdminById(Integer id) {
        Admin admin = adminMapper.selectById(id);
        return admin;
    }
}

13.4.4mapper

AdminMapper接口

package com.ymk.hotelsystem.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ymk.hotelsystem.pojo.Admin;
import org.springframework.stereotype.Repository;

/**
 * @Desc
 */
@Repository
public interface AdminMapper extends BaseMapper<Admin> {
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Vue详解及综合案例 的相关文章

随机推荐

  • PowerBI入门学习笔记

    下载安装 Win10系统 在微软商店里直接下载PowerBI desktop 打开即可 界面如下 接下来导入后面要用到的数据 我目前用的都是Excel文件 获取数据 选中后选择要导入的若干个工作表 点击 加载数据 就进入到power que
  • vue+element实现双向描点 反向联动

    前端项目里经常会有锚点得操作 以及反向联动的效果 就是一个菜单 点击会定位到一个块上 滚动的当前块的时候 菜单会出现定位的效果 差不多就是这种动起来的效果 由于不太懂之前的逻辑 今天又从重新看了下 上代码 html 滚动的区域
  • 关于Python中pip install 各种包下载不下来的问题解决办法

    你们有可能报安装不成功或者下面这个问题 已经安装了但并非在你的Python安装路径下 C Users xxx gt pip install ddt Requirement already satisfied ddt in e anacond
  • redis 实现乐观锁

    redis是单线程程序但是支持多进程同时访问同一个redis服务 这个时候就需要锁机制来处理一些并发问题 redis提供了watch指令来实现乐观锁 watch和事务配合使用 往往写在multi之前 用来监视一个key 比如watch mo
  • 实战wxPython:047 - Book控件(第一部分)

    在wxPython中 book控件允许用户在各种面板之间切换 最常见的例子是带有选项卡界面的浏览器和系统选项对话框 本文将向您介绍这些控件的创建和基本配置 wxPython目前内置了多个这样的控件 除文章 wxPython 高级控件之选项卡
  • 看完这篇 教你玩转渗透测试靶机Vulnhub——Grotesque:3.0.1

    Vulnhub靶机Grotesque 3 0 1渗透测试详解 Vulnhub靶机介绍 Vulnhub靶机下载 Vulnhub靶机安装 信息收集 漏洞发现 LFI漏洞利用 本地文件包含漏洞 SSH登入 提权 获取FLAG Vulnhub靶机渗
  • pyqt 万能简易模板(四)

    本文将介绍一些pyqt5基本使用技巧 不借助Qtdesigner 而是全部用代码编写 将实现页面布局 窗口自适应 字体自适应等功能 一般的简易工程均可使用 简单高效 对于pyqt5的一些基本技巧 本文内容基本够用 可以快速实现自己想要的界面
  • Java中的如何检测字符串是否相等

    文章目录 0 写在前面 1 介绍 2 举例 3 写在后面 0 写在前面 实际业务中有时候得检测字符串是否相等的场景 例如在数据库中提取uuid 检测两个uuid是否相等就需要用到这个地方 1 介绍 可以使用equals方法检测两个字符串是否
  • 数据预处理、特征工程和特征学习

    神经网络的数据预处理 数据预处理的目的是使原始数据更适于用神经网络处理 包括向量化 标准化 处理缺失值和特征提取 1 向量化 神经网络的所有输入和目标都必须是浮点数张量 特定情况下为整数张量 无论处理什么数据 都必须先将其转换为张量 这一步
  • Spring getBean方法源码解析

    User user User beanFactory getBean user 注 User为一普通bean 查看方法 AbstractBeanFactory getBean public Object getBean String nam
  • 朴素贝叶斯 Naive Bayes

    Naive Bayes 特点 朴素贝叶斯是典型的生成学习方法 朴素贝叶斯的基本假设是条件独立性 强假设 若条件之间存在概率依存关系 模型变为贝叶斯网络 基于上一条的假设 朴素贝叶斯方法高效 但分类性能受损 将输入的 x 分类到后验概率最大的
  • 五号黯区靶场 mysql 注入之limit注入记录

    前言 感谢五号黯区团队 limit 功能 通常在分页处 比如page 字段 可能存在 limit注入 不存在order by http bug cc 86 limit limit php p 1 访问靶场如下图 原数据库语句如下 selec
  • python数组做参数_python函数传递数组参数吗

    函数是任何一门编程语言都不可缺少的 而且也是非常重要的一部分 Python中函数的参数 可以分为以下几类 位置参数 有时也称必备参数 指的是必须按照正确的顺序将实际参数传到函数中 换句话说 调用函数时传入实际参数的数量和位置都 必须和定义函
  • vue 项目使用通过经纬度显示地图

    传入经纬度 和缩放值
  • 最新的一篇视觉Transformer综述!

    点击下方卡片 关注 CVer 公众号 AI CV重磅干货 第一时间送达 作者 闪闪红星闪闪 已授权转载 源 知乎 https zhuanlan zhihu com p 433048484 CVer公众号已汇集三篇视觉Transformer
  • Tizen手机系统新“鲶鱼”

    转自 http mobile 51cto com comment 380564 htm 手机操作系统很有可能在2013年上演 巅峰之战 Tizen等基于HTML5技术的新产品 将成为搅动市场的鲶鱼 AD 2013云计算架构师峰会课程资料下载
  • 回归分析的假设条件

    数据什么样就能扔进回归分析回归分析
  • registry :分支操作值Archive有什么用?

    1 美图 2 背景 想知道registry 分支操作值Archive有什么用 于是点击修改了一下 修改之后看着好像没什么变化 结果发现不能更改了 只能添加新的版本了 但是服务器上改了之后导致 Registry THERE IS NOT VE
  • 数据库基础——10.子查询

    这篇文章来讲一下数据库的子查询 目录 1 需求分析与问题解决 1 1 实际问题 1 2 子查询的基本使用 1 3 子查询的分类 2 单行子查询 2 1 单行比较操作符 2 2 代码示例 2 3 HAVING 中的子查询 2 4 CASE中的
  • Vue详解及综合案例

    一 Vue简介 1 1 简介 Vue 读音 vju 类似于 view 是一套用于构建用户界面的渐进式的js框架 发布于 2014 年 2 月 与其它大型框架不同的是 Vue 被设计为可以自底向上逐层应用 Vue 的核心库只关注视图层 不仅易