Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式的js框架,发布于 2014 年 2 月。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库(如:vue-router,vue-resource,vuex)或既有项目整合。
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实例;
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进行绑定的。
轻量级,体积小是一个重要指标。Vue.js 压缩后有只有 20多kb (Angular 压缩后 56kb+,React 压缩后 44kb+)
移动优先。更适合移动端,比如移动端的 Touch 事件
易上手,学习曲线平稳,文档齐全
吸取了 Angular(模块化)和 React(虚拟 DOM)的长处,并拥有自己独特的功能,如:计算属性
开源,社区活跃度高
当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是为什么 Vue 不支持 IE8 以及更低版本浏览器。
这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。这里需要注意的问题是浏览器控制台在打印数据对象时 getter/setter 的格式化并不同,所以你可能需要安装 vue-devtools 来获取更加友好的检查接口。
每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。
页面上每个独立的可交互的区域视为一个组件
每个组件对应一个工程目录,组件所需的各种资源在这个目录下就近维护
页面不过是组件的容器,组件可以嵌套自由组合(复用)形成完整的页面
注意:cdn是一种加速策略,能够快速的提供js文件
创建一个div,id是app
2.3 创建vue对象,设计对象的内容
其中该vue对象,绑定了页面中id是app的那个div
# el: element的简称,也就是Vue实例挂载的元素节点,值可以是 CSS 选择符,或实际 HTML 元素,或返回 HTML 元素的函数。
# data: 用于提供数据的对象,里面存放键值对数据。
说明:
这里有几点重要说明:
new Vue 我们创建了Vue对象;
el 指定了绑定DOM,接管DOM操作;
data:用于提供数据的对象,里面存放键值对数据
v-model 重点 可以实现数据双向绑定,改变了这里的值,其他地方也根据改变;
{{xxxxx}} 显示数据;
{{ title }}
插值表达式的作用是在View中获得Model中的内容
Model中的内容如下:
new Vue({
el:"#app",
data:{
title:"hello world!"
},
methods:{
sayHello:function(){
return "hello vue";
}
}
});
{{title}}
此时,页面上将会显示"Hello world!"
{{[1,2,3,4][2]}}
此时,页面上会显示“3”,也就是数组中的第三个元素被获取。
{{ {"name":"xiaoyu","age":20}.age }}
此时,页面上会显示“20”,也就是对象中age属性的值。
{{ sayHello()}}
此时,页面上会显示“hello vue”,也就是调用了vue对象中的sayHello方法,并展示了方法的返回值。
Vue.js通过加载js,实现对页面的快速渲染。vue封装的js该如何使用? 就必须了解MVVM双向数据绑定模式。Vue将视图层和数据层分离,通过MVVM建立视图层和数据层的连接。其中,插值表达式是一种连接方式,可以通过插值表达式以多种方式,快速的从数据层获取数据并展示在视图层上。数据层Vue对象,也是由很多部分组成,比如之前介绍的el,data,methods等,以及之后要介绍的mount,computed等。
Vue中的分支语句v-if非常好理解,逻辑跟Java中的if-else相同。v-if语句块包含以下内容:
v-if
v-else
v-else-if
接下来以一个简单例子即可理解:
今天天气很舒服!
今天天气很燥热!晚上要去放松一下!
晚上只能自嗨!
从这个例子可以看出,vue对象中的data提供了分支的条件。根据条件,如果是true,则v-if的内容就会显示,反之不显示。
v-if和v-show之间有着看似相同的效果,但优化上却有区别。先看下面这个例子:
有钱!
有钱!
通过点击“今晚彩票开奖”按钮,能切换rich的值,此时发现,v-if和v-show的显示状态都会来回切换。看起来是一样的,但通过查看控制台代码发现,v-show实际会将p标签的css样式的display属性设为none来达到隐藏的效果。
而v-if是直接在页面上添加和删除p标签来达到效果,因此v-show在反复切换的应用场景下,效率比v-if更高。
Vue中的循环关键字并没有Java的那么多,只有v-for,但用法上有多种。接下来我们来逐一介绍。
我们需要定义数据源,然后通过v-for来遍历数据源,再使用差值表达式输出数据。
- {{item.date}}
在这个例子中,数据源提供了一个数组。视图层通过v-for来循环输出多个li标签,非常简单。
- {{i}}{{a}}
此时的i就是每次循环的循环变量 ,从0开始一直到元素个数-1
- {{name}}--{{value}}--{{index}}
value、name、index 这几个字符可以自己定义,分别表示每次循环内容的值、键、序号。
value: 循环中每条数据的值 小鱼、20、如花
name: 循环中每天数据的键 username、age、girl
index: 循环的序号,从0开始
页面效果如下: |
---|
姓名
年龄
联系方式
{{stu.name}}
{{stu.age}}
{{stu.phone}}
可以清楚的看到,此时数据源是一个student数组,通过两层v-for循环,外层遍历数组中的每个student对象,内层v-for遍历每个对象的v、k、i。
Vue提供了多个关键字,能快速的将数据对象中的值绑定在视图层中。
通过v-model将标签的value值与vue对象中的data属性值进行绑定。
{{title}}
new Vue({
el:'#app',
data:{
title:"hello vue"
}
})
此时input标签中加入了“v-model='title'”,表示input的value值与vue对象中的title属性绑定,当在input输入框中输入内容会实时修改title的值。于是{{title}}插值表达式能实时输出input输入框内的值。
页面效果如下: |
---|
我们知道插值表达式是不能写在html的标签的属性内的,那如果一定要用vue中的属性作为html标签的属性的内容,就可以通过v-bind进行属性绑定。
new Vue({
el:'#app',
data:{
link:'http://www.baidu.com'
}
})
这样,a标签内的href属性就可以使用vue对象中的属性值。
注意: v-bind也可以简写,使用冒号“:”来代替。
等价于
关于事件,要把握好三个步骤:设参、传参和接参。
sum={{sum}}
{{sum>10?'总数大于10':'总数不大于10'}}
从这里例子中:
设参:
传参:
increase:function(s)
接参:
this.sum+=s
注意:increase:function(s){ }函数可以写成increase(s){ }
接下来我们来看一下VUE中如何进行事件绑定。
通过配合具体的事件名,来绑定vue中定义的函数
此时,该按钮,在点击时将会调用Vue对象中定义的changeMajor方法。
注意: v-on也可以简写,使用"@"替代。
可以使用Vue中定义好的事件修饰符,快速达到效果。查看以下例子:
x:{{x}}
y:{{y}}
鼠标移动到此即停止
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");
}
}
})
当鼠标经过P标签区域内时,区域内就会显示X和Y轴的坐标,如果经过P标签内的Span标签内时,此时会调用事件属性mousemove.stop预定的效果,鼠标移动的效果将会被取消,X和Y不再显示信息。
计算属性的重点突出在 属性 两个字上(属性是名词),首先它是个 属性 其次这个属性有 计算 的能力(计算是动词),这里的 计算 就是个函数;简单点说,它就是一个能够将计算结果缓存起来的属性(将行为转化成了静态的属性),仅此而已;
布局篇 计算属性
调用当前时间的方法:{{currentTime1()}}
当前时间的计算属性:{{currentTime2}}
说明
methods:定义方法,调用方法使用 currentTime1(),需要带括号
computed:定义计算属性,调用属性使用 currentTime2,不需要带括号;this.message 是为了能够让 currentTime2 观察到数据变化而变化
注意:methods 和 computed 里不能重名
仔细看图中说明,观察其中的差异
调用方法时,每次都需要进行计算,既然有计算过程则必定产生系统开销,那如果这个结果是不经常变化的呢?此时就可以考虑将这个结果缓存起来,采用计算属性可以很方便的做到这一点;计算属性的主要特性就是为了将不经常变化的计算结果进行缓存,以节约我们的系统开销
Vue的组件化设计思想借鉴了Java的面向对象思想。Java认为万物皆对象,在Vue中,万物皆组件。
也就是说,在实际的vue项目中,以及使用了Vue框架的项目中,Vue的对象都会以组件的形式出现,能被反复使用。
要想实现组件化,需要在页面中注册组件:关于注册的方式有两种,分别是全局注册和本地注册。
vue组件的全局注册
vue的全局注册,也就意味着在页面的任意一个被vue绑定过的div中,都可以使用全局注册了的vue组件。
但是,如果是对vue组件进行本地注册,那么在其他被vue绑定的div中,不能使用该组件。
vue组件的本地(局部)注册
这是一个完整的Vue组件。该组件包含了三个部分:template(html视图层内容)、script(Model层)、style(CSS样式)。这样封装好的组件可以被复用,也可以作为其他组件的组成部分而被封装——Java的面向对象再次体现。
特点1: template标签内,必须有且只能有一个根标签。
特点2: componet中注册的组件中的data,必须是已函数的形式。
如下:
data:function(){
return {
title:"hello vue"
}
}
Vue中的组件也是有生命周期的。一个Vue组件会经历:初始化、创建、绑定、更新、销毁等阶段,不同的阶段,都会有相应的生命周期钩子函数被调用。
组件的生命周期钩子 |
---|
生命周期主要有三个阶段:
- 一,初始化显示;(重要勾子 mounted 网页加载完毕触发)
- 二,更新显示;(重要勾子beforeUpdate 数据属性更新前)
- 三,死亡;(重要勾子beforeDestroy vm死亡前)
每个生命周期都会有对应的生命周期的函数,或者叫做勾子函数;
Title
每个 Vue 实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、
将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,
这给了用户在不同阶段添加自己的代码的机会。
{{name}}
cli: Command Line 命令行工具,vue-cli就是vue的命令行工具,也称之为脚手架,使用vue-cli提供的各种命令可以拉取、创建、运行我们需要使用到的框架,比如webpack、Element UI、Element Admin等等。那么要想使用vue-cli命令,需要先安装node.js。
node.js的介绍
node.js提供了前端程序的运行环境,可以把node.js理解成是运行前端程序的服务器。
node.js的安装
从官网下载安装即可: 下载 | Node.js 中文网
测试node.js是否安装成功: 在DOS窗口中输入“node -v” 查看版本,如果看到版本,就表示安装成功。 |
---|
使用如下命令安装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
dos命令输入vue ui会进入如下图:
创建项目 不保存预设 然后 后台就开始下载文件
创建好之后 页面跳转到仪表盘
可以在这里 安装一些插件 比如 axios element-ui 等等
这是已经安装的插件
点 添加插件
安装 ajax 插件 axios
然后 可以关掉仪表盘,使用vscode 打开 刚才创建的工程 使用 vue run serve 启动工程,
打开VSCode,文件—>首选项—>配置用户代码片段
输入一下代码:配置完成后新建.vue文件输入vue回车就会生成以下代码。
{
"Print to console": {
"prefix": "vue",
"body": [
"",
"",
" $5",
"",
"",
"",
""
],
"description": "生成vue模板"
}
}
就像maven一样,vue为我们提供了一些官方项目骨架。使用vue list命令可以列出当前官方提供的骨架,可以使用这些骨架来快速搭建出项目。
vue list
完事 用vscode 打开项目文件夹
父组件
消息订阅与发布组件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); //这样 回调就只执行一次
父组件向子组件传递标签,通过slot 插槽实现
主要作用:
某一个区块,先占位,然后可以动态的搞个标签进去,方便切换该位置的内容,无需再搞多个页面。
父组件 直接定义标签p 注意 slot 属性
子组件 使用 slot 标签 占位置 注意 slot 标签的name 属性,当父组件没有传对应的插槽(属性值对应)内容时,会显示插槽默认内容
常用命令:
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,页面效果如下: |
---|
npm install vue-router --save
创建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: ' '
})
在 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 中的路由写法
通过 router-link 向路由配置发送请求 ,结果会在 router-view 显示
Menu1.vue是在前面创建的views文件夹下 继续嵌套 下级页面 配置嵌套路由,其他代码有vue片段生成
Menu1.vue
{{msg}}
-
子菜单1
-
子菜单2
在路由配置文件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片段生成
子菜单1
SubMenu2.vue,其他代码vue片段生成
子菜单2
当我们 在 子菜单1 和 子菜单2 的 文本输入框输入内容时,发现 跳转之后内容没有了,这种情况也经常遇到
我们切换不同的组件时,希望 组件内输入的内容不要丢失 ,怎么解决?只需要 使用keep-alive 标签
app.vue显示页面的配置:
注意:上述路由需要再定义个div并在里面写
标签
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
编号
账号
密码
{{ admin.id }}
{{ admin.username }}
{{ admin.password }}
当点击名字时会显示该用户信息,用到嵌套路由,在views下创建AdminDetail.vue文件,代码如下:
Id:{{ $route.params.id }}
{{ admin.username }}
{{ admin.password }}
{{ admin.type }}
{{ admin.create_time }}
只需要在上面的Admin.vue文件中添加一些代码就行。
编号
账号
密码
测试编程式路由
{{ admin.id }}
{{ admin.username }}
{{ admin.password }}
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
由于 Vue.js 是一个 视图层框架 并且作者(尤雨溪)严格准守 SoC (关注度分离原则),所以 Vue.js 并不包含 AJAX 的通信功能,为了解决通信问题,作者单独开发了一个名为 vue-resource 的插件,不过在进入 2.0 版本以后停止了对该插件的维护并推荐了 Axios 框架
npm install --save axios vue-axios
在项目中使用axios模块
import Vue from 'vue'
import axios from 'axios'
import VueAxios from 'vue-axios'
Vue.use(VueAxios, axios)
在使用axios时,要注意到配置选项中包含params和data两者
1.params是添加到url的请求字符串中的,用于get请求;
2.而data是添加到请求体(body)中的, 用于post请求;
this.$axios({ //这种格式,是需要每个参数都弄成对象的
methods: 'POST',
url: '#',
data: {
key1: val1,
key2: val2
},
timeout: 1000,
baseURL:'xxxx'
...//其他相关配置
})
this.$axios({
methods: 'get',
url: '#',
params: { //注意是params,get请求必须写上params,不能写data
key1: val1,
key2: val2
},
timeout: 1000,
baseURL:'xxxx'
...//其他相关配置
})
注意:
在get请求下,参数需要使用【params】来设置,
而post请求中,是使用data来传递的
this.$axios.get('url')
this.$axios.post('url')
注意:这种直接没有参数的,就写个url就行了
get请求直接写参数是错误写法,会导致请求无法携带参数
如果get请求有参数,必须使用params:{}括起来
如果带token的话可以在app.vue中的mounted里面设置全局的
mounted(){
this.axios.defaults.headers.common['token'] = 6666;
}
创建一个.vue文件书写一下代码:
编号
账号
密码
{{ admin.id }}
{{ admin.username }}
{{ admin.password }}
在spring-mvc.xml中加入上述这一段。其中,allowed-origins指的是允许的访问源的域名,"*"表示任何人都可以访问,也可以指明具体的域名
原因:默认情况下发送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 是一个专为 Vue.js 应用程序开发的 状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
1.先在创建一个没有使用 vuex的组件 MyVuex.vue
vuex
当前数字: {{number}} is {{evenOrOdd}}
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
当前数字: {{$store.state.number}} is {{$store.getters.evenOrOdd}}
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中的
在src下创建pages文件夹,然后创建Login.vue
欢迎登录
登录
在src下的pages文件夹下面创建Home.vue
导航一
分组一
用户管理
房屋管理
选项3
选项4
选项4-1
导航二
分组一
选项1
选项2
选项3
选项4
选项4-1
导航三
分组一
选项1
选项2
选项3
选项4
选项4-1
查看
新增
删除
{{ this.$store.getters.getAdmin.username }}
注意:当用户登录成功后,后端响应一个token,前端接受token将token存到本地在 beforeCreate()里设置了全局的token,这样就不用每次请求都设置token请求头了。
在src下的pages文件夹下面创建AdminList.vue
根据需要自行设置每页显示条数,只需更改data里面的pageSize属性。
查询
新增
在src文件夹下创建components文件夹,如果前面policy.js配置文件时已经创建就直接在component是下创建singleUpload.vue.
点击上传
只能上传jpg/png文件,且不超过10MB
在src下的pages文件夹下面创建AdminAdd.vue
该页面是通过点击新增寻找/home/adminAdd路径的路由,可以看到改页面弹出在home页面之上,这是因为在home的路由配置通过children[{}]里面设置了嵌套路由。
提交
取消
该功能使用的弹出框代码在Home.vue中。
methods中handleClickDelete()方法里.
在src下的pages文件夹下面创建AdminEdit.vue
提交
取消
目录
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-devtools
runtime
true
org.springframework.boot
spring-boot-configuration-processor
true
org.projectlombok
lombok
1.18.20
true
org.springframework.boot
spring-boot-starter-test
test
mysql
mysql-connector-java
runtime
com.baomidou
mybatis-plus-generator
3.3.2
org.apache.velocity
velocity
1.7
com.baomidou
mybatis-plus-extension
3.4.2
mysql
mysql-connector-java
runtime
com.alibaba
fastjson
1.2.56
com.baomidou
mybatis-plus-boot-starter
3.4.2
com.baomidou
mybatis-plus-generator
3.3.2
com.github.xiaoymin
knife4j-spring-boot-starter
2.0.7
org.springframework.boot
spring-boot-starter-aop
com.alibaba.cloud
aliyun-oss-spring-boot-starter
1.0.0
com.auth0
java-jwt
3.4.0
io.jsonwebtoken
jjwt
0.7.0
# 服务端口号
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
全局拦截器,此代码在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 {
}
下面代码在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;
}
}
下面代码在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);
}
}
下面代码在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
下面的代码在common包下,类名LayuiPageVo
作用:
用来封装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 {
private Integer code; // 0 表示成功
private String msg;
private Long count;
private List data;
}
下面代码在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);
}
}
下面代码在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("[email protected]")
.version("1.0")
.build())
//分组名称
.groupName("2.X版本")
.select()
//这里指定Controller扫描包路径
.apis(RequestHandlerSelectors.basePackage("com.ymk.demo22.controller"))
.paths(PathSelectors.any())
.build();
return docket;
}
}
下面代码在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() {
@Override
public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString("");
}
});
return objectMapper;
}
}
下面代码在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);
}
}
下面代码在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;
}
下面代码在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 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();
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 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 adminList(@RequestParam Map map){
System.out.println(map);
LayuiPageVo 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 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);
}
}
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 adminList(Map 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 queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", username).eq("password", password);
return adminMapper.selectOne(queryWrapper);
}
/**
* 分页加条件查询
* @param map
* @return
*/
@Override
public LayuiPageVo adminList(Map map) {
//加上空字符串变成字符串再转成Integer类型
Integer page = Integer.parseInt(map.get("page")+"");
Integer limit = Integer.parseInt(map.get("limit")+"");
Page adminPage = new Page<>(page,limit);
QueryWrapper queryWrapper = new QueryWrapper<>();
// 通过遍历的方式将map集合中不参加模糊查询的键值过滤,将模糊查询放到条件构造器中
for (Map.Entry 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 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 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;
}
}
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 {
}