①.导入sql文件
1.选中数据库test,右键选择运行SQL文件
2.选中路径导入
3.刷新页面
pn_admin_menu 菜单表
有一级菜单和二级菜单,主菜单和副菜单的属性很相似,故在一个表。
pn_admin_user 用户表
看一下sql语句:
登录成功之后会先找用户的权限字段:
找到该用户对应的权限菜单:
权限字段拿到之后,会通过它查一级和二级菜单(一般不会超过二级),最后返回给页面的是嵌套的数据。但这样的数据,数据库给不了。
现在先查一级菜单:
查一级菜单,也就是pid=0 对应的sql语句:select * from pn_admin_menu pam WHERE pam.pid
二级菜单:select * from pn_admin_menu pam WHERE pam.pid != 0
现在是查用户有的菜单:
如下是匹配的二级菜单:
拿到登录成功的用户的一级菜单就可以遍历,找对应的二级菜单,然后塞进一级菜单。
②.导入后台服务接口
导入的项目会重新创建一个项目,项目名这里用smStep2
设置环境:
引入项目的依赖:
选择Library,选择Tomcat运行环境:
其他服务接口继承BaseServlet。
这里的过滤器主要允许跨域请求和登录效验。
先把服务跑起来:
点击+号后,选择Tomcat Serve里的Local,添加Tomcat实例。
创建Artifacts:
选择完之后,点击ok
加载当前的项目:
运行服务器。
试试这几个服务接口有没有问题:
先访问登录服务:http://localhost:8080/login?username=abc&pwd=abc
页面显示 {"returnCode":20001,"returnMsg":"用户名或密码错误"}
访问:http://localhost:8080/login?username=%E6%B5%8B%E8%AF%951&pwd=abc123
页面显示 {"returnCode":10000,"returnMsg":"操作成功"}
登录成功后,登录接口会将当前用户登录信息和用户权限信息放到session。
//登录访问控制 session.setAttribute("loginUser", loginUser); //权限访问控制 展示菜单 session.setAttribute("userRoles", userRole);
然后需要取用户session信息时,取出来拼成json格式给界面返回。(GetMenuServlet接口)
访问:http://localhost:8080/getmenu
页面显示:
{"returnCode":10000,"returnData":[{"glyphicon":"el-icon-user","menuid":11,"menuname":"管理中心","menuurl":"#","pid":0,"pname":"","submenu":[{"glyphicon":"glyphicon glyphicon-lock","menuid":11001,"menuname":"用户管理","menuurl":"/users","pid":11,"pname":"","submenu":[]},{"glyphicon":"glyphicon glyphicon-lock","menuid":11002,"menuname":"菜单管理","menuurl":"/menus","pid":11,"pname":"","submenu":[]}]},{"glyphicon":"el-icon-s-order","menuid":12,"menuname":"模版管理","menuurl":"#","pid":0,"pname":"","submenu":[{"glyphicon":"","menuid":12001,"menuname":"模版配置","menuurl":"/template/cfglist","pid":12,"pname":"","submenu":[]},{"glyphicon":"","menuid":12002,"menuname":"模版列表","menuurl":"/template/templatelist","pid":12,"pname":"","submenu":[]}]},{"glyphicon":"el-icon-data-line","menuid":13,"menuname":"广告管理","menuurl":"#","pid":0,"pname":"","submenu":[{"glyphicon":"","menuid":13001,"menuname":"首页置顶广告","menuurl":"/banner/toplist","pid":13,"pname":"","submenu":[]}]},{"glyphicon":"el-icon-timer","menuid":14,"menuname":"游戏管理","menuurl":"#","pid":0,"pname":"","submenu":[{"glyphicon":"glyphicon glyphicon-eye-close","menuid":14001,"menuname":"游戏配置","menuurl":"/game/gamelist","pid":14,"pname":"","submenu":[]}]},{"glyphicon":"el-icon-ship","menuid":15,"menuname":"合作公司管理","menuurl":"#","pid":0,"pname":"","submenu":[{"glyphicon":"glyphicon glyphicon-send","menuid":15001,"menuname":"合作公司管理","menuurl":"/partner/list","pid":15,"pname":"","submenu":[]}]},{"glyphicon":"el-icon-setting","menuid":16,"menuname":"渠道版本","menuurl":"#","pid":0,"pname":"","submenu":[{"glyphicon":"glyphicon glyphicon-screenshot","menuid":16001,"menuname":"渠道管理","menuurl":"/channel/list","pid":16,"pname":"","submenu":[]},{"glyphicon":"glyphicon glyphicon-bullhorn","menuid":16002,"menuname":"渠道分类管理","menuurl":"/channel/typelist","pid":16,"pname":"","submenu":[]}]}],"returnMsg":"操作成功"}
新建测试类MyTest:
按照①.的设想,写一下代码:
public class MyTest { public static void main(String[] args) { LoginDao ld = new LoginDaoImpl(); //查询用户的权限信息 11001,11002,12001,12002,12003,13001,14001,14002,15001,16001,16002,19001,19002,11,12,13,14,15,16 User user = new User(); user.setUserId(9); String userRole = ld.getUserRole(user); MenuDao md = new MenuDaoImpl(); //查一级菜单 List
控制台输出:
select mu.mid,mu.menuname,mu.pid,mu.url,mu.glyphicon from pn_admin_menu mu where mu.pid=0 and mu.mid in (11001,11002,12001,12002,12003,13001,14001,14002,15001,16001,16002,19001,19002,11,12,13,14,15,16) select mu.mid,mu.menuname,mu.pid,mu.url,mu.glyphicon from pn_admin_menu mu where mu.pid!=0 and mu.mid in (11001,11002,12001,12002,12003,13001,14001,14002,15001,16001,16002,19001,19002,11,12,13,14,15,16) [Menu{menuid=11, menuname='管理中心', menuurl='#', pid=0, pname='', glyphicon='el-icon-user', submenu=[]}, Menu{menuid=12, menuname='模版管理', menuurl='#', pid=0, pname='', glyphicon='el-icon-s-order', submenu=[]}, Menu{menuid=13, menuname='广告管理', menuurl='#', pid=0, pname='', glyphicon='el-icon-data-line', submenu=[]}, Menu{menuid=14, menuname='游戏管理', menuurl='#', pid=0, pname='', glyphicon='el-icon-timer', submenu=[]}, Menu{menuid=15, menuname='合作公司管理', menuurl='#', pid=0, pname='', glyphicon='el-icon-ship', submenu=[]}, Menu{menuid=16, menuname='渠道版本', menuurl='#', pid=0, pname='', glyphicon='el-icon-setting', submenu=[]}] [Menu{menuid=11001, menuname='用户管理', menuurl='/users', pid=11, pname='', glyphicon='glyphicon glyphicon-lock', submenu=[]}, Menu{menuid=11002, menuname='菜单管理', menuurl='/menus', pid=11, pname='', glyphicon='glyphicon glyphicon-lock', submenu=[]}, Menu{menuid=12001, menuname='模版配置', menuurl='/template/cfglist', pid=12, pname='', glyphicon='', submenu=[]}, Menu{menuid=12002, menuname='模版列表', menuurl='/template/templatelist', pid=12, pname='', glyphicon='', submenu=[]}, Menu{menuid=13001, menuname='首页置顶广告', menuurl='/banner/toplist', pid=13, pname='', glyphicon='', submenu=[]}, Menu{menuid=14001, menuname='游戏配置', menuurl='/game/gamelist', pid=14, pname='', glyphicon='glyphicon glyphicon-eye-close', submenu=[]}, Menu{menuid=15001, menuname='合作公司管理', menuurl='/partner/list', pid=15, pname='', glyphicon='glyphicon glyphicon-send', submenu=[]}, Menu{menuid=16001, menuname='渠道管理', menuurl='/channel/list', pid=16, pname='', glyphicon='glyphicon glyphicon-screenshot', submenu=[]}, Menu{menuid=16002, menuname='渠道分类管理', menuurl='/channel/typelist', pid=16, pname='', glyphicon='glyphicon glyphicon-bullhorn', submenu=[]}, Menu{menuid=19001, menuname='标签列表', menuurl='/tag/taglist', pid=19, pname='', glyphicon='glyphicon glyphicon-tags', submenu=[]}] ------------------------------------ [Menu{menuid=11, menuname='管理中心', menuurl='#', pid=0, pname='', glyphicon='el-icon-user', submenu=[]}, Menu{menuid=12, menuname='模版管理', menuurl='#', pid=0, pname='', glyphicon='el-icon-s-order', submenu=[]}, Menu{menuid=13, menuname='广告管理', menuurl='#', pid=0, pname='', glyphicon='el-icon-data-line', submenu=[]}, Menu{menuid=14, menuname='游戏管理', menuurl='#', pid=0, pname='', glyphicon='el-icon-timer', submenu=[]}, Menu{menuid=15, menuname='合作公司管理', menuurl='#', pid=0, pname='', glyphicon='el-icon-ship', submenu=[]}, Menu{menuid=16, menuname='渠道版本', menuurl='#', pid=0, pname='', glyphicon='el-icon-setting', submenu=[]}]
之前submenu是空的,现在就有了数据了。
最后只需做格式转换,然后把这样的菜单数据返回给前端,前端就可以遍历使用了。
③.使用postman
信息太多,用postman工具。
新建集合:
起名newtest。
新建项目,new后选择Request:
点击确定。
点击send。
控制台输出:{"returnCode":10000,"returnMsg":"操作成功"}
新建queryUserMenu项目:
submenu里放的子菜单数据。配合前端的vue,两层v-for循环就可将数据遍历出来。
第二个服务接口GetMenuServlet作用:取登录的时候查出来的菜单数据。菜单数据的组织是在登录时就已经放好,登录成功后直接从session里拿。
后端剩下的就是用户模块和菜单模块的增删改查。
新建queryMenu(菜单模块查询):
可传page去翻页:http://localhost:8080/menu/query?page=2
再测试一下菜单的增加功能:
对应的数据库里,菜单表多了一条数据。
后台服务接口已经开发结束,接下来是前端。
④.前端部分
打开视图界面工具可访问:http://localhost:8000/dashboard
打开HBUilder,将配置文件的路由改为打开页面先显示登录页面:
{path:'/',redirect:"/login"}
现在要使前端的登录和后台登录服务接口真正结合起来:
回到登录组件loginPage.vue:
如果验证成功,发Ajax请求去后台。找到后台登录服务接口地址(从postman复制,建的第一个项目就是登录),填入。
现在是跨域访问,需要绝对路径。
发送的参数要和后台搭配起来,后台用的是username和pwd。
同时数据是从界面拿过来的,且拿过来之后用qs转一下再发送
登录登录 重置
同时后端的登录服务接口也要允许它的跨域访问,修改一下:
跨域访问功能移到了过滤器,CheckLogin类:
resp.setHeader("Access-Control-Allow-Origin", "http://localhost:8088");
再检查一下前端服务器地址:
测试:
登录界面先来一个错的:
用户名:abcabc
密码:123456
(Header请求头和响应头 Payload给后端传递的参数
Preview后端返回的参数 Response后台返回的响应)
返回的响应是:{"returnCode":20001,"returnMsg":"用户名或密码错误"}
再来个正确的:
用户名:测试1
密码:abc123
返回的响应:{"returnCode":10000,"returnMsg":"操作成功"}
至此跨域访问成功。
如果用户名或者密码错误,要有一个体现,让用户知道。
⑤.使用elementUI
组件里Notice部分的Message部分的不同状态。
this.$message.error('错了哦,这是一条错误消息');
在组件里添加:
.then((ret)=>{ console.log(ret); if(ret.data.returnCode == 20001){ this.$message.error(returnMsg); } })
最后用户名密码正确则跳转到main组件。
.then((ret)=>{ console.log(ret); if(ret.data.returnCode == 20001){ this.$message.error(ret.data.returnMsg); }else if(ret.data.returnCode == 10000){ this.$router.push("/main"); } })
使用脚手架之后,前端项目和后端项目完全隔离开了。
前端服务和后端服务之间没什么关联,就是两套应用程序通过http的异步请求做数据交互。
每个组件访问后端都要写完整的访问路径,很麻烦,axios提供了新的方法。(官网可看)
后台通过cookie保存浏览器和它的对应关系,故往后台发消息时,cookie也要允许去发送。
在main.js,加全局配置:
import Vue from 'vue' import App from './App.vue' import router from './router' import './plugins/element.js' import qs from 'qs' import axios from 'axios' Vue.config.productionTip = false /*为axios配置全局参数*/ //服务器路径 axios.defaults.baseURL="http://localhost:8080/" //跨域带cookie axios.defaults.withCredentials = true Vue.prototype.$qs = qs; Vue.prototype.$axios = axios new Vue({ router, render: h => h(App) }).$mount('#app')
然后发ajax时:
this.$axios.get("/login?"+this.$qs.stringify(this.form))
①.登录成功后,发送axios请求,去请求GetMenuServlet接口。
mainpage组件:
请求不需要参数。
先打印出数据看看:
mounted(){ console.log("页面加载结束后执行"); this.$axios.get("/getmenu") .then((ret)=>{ console.log(ret); }) .catch((err)=>{ console.log(err); }); }
后台返回的数据:
假数据删除,留一个空集合:
data () { return { menuList:[] }; }
从poetman里复制一组该集合,改一下key:
{{menu.menuname}} {{subm.menuname}}
接下来将取回的数据遍历到页面:
mounted(){ console.log("页面加载结束后执行"); this.$axios.get("/getmenu") .then((ret)=>{ console.log(ret); if(ret.data.returnCode==10000){ this.menuList = ret.data.returnData; } }) .catch((err)=>{ console.log(err); }); }
效果如下:
②.点击菜单后要换到对应的组件:
每个菜单有自己的url地址,后台存的访问地址实际是前端的组件地址。
如上,用户管理对应/users组件,菜单管理对应/menus组件。
故前端组件需要把他俩加上。
为方便管理,在components目录下新建controlCenter(管理中心)目录。里面放两个模块,新建menus.vue和users.vue
用户模块
菜单模块
点击对应菜单实际是往这两个模块跳。
先导入这两个组件,打开index.js:
import Users from '../components/controlCenter/menus.vue' import Menus from '../components/controlCenter/menus.vue'
并配上对应的路由,和后台保存的地址搭配起来:
{path:'/users',component:Users}, {path:'/menus',component:Menus},
先在浏览器访问这两个组件,看有没有问题。
接下来就做点击跳到这两个组件:
在elementUI里的导航菜单有vue-router模式
加一个router属性。
{{menu.menuname}} {{subm.menuname}}
此时,点击导航条的管理中心部分的用户管理和菜单管理,会跳到相应的组件。
③.配子路由
跳到用户或者菜单管理,也应该是上左右布局。每个组件里相同的内容都要写一遍。对于这种问题,还是路由解决。路由可配子路由,即在当前组件里替换某些部分,而不是整个替换。
之前在App.vue里
是所有的东西都替换到了根组件App.vue里。
若想替换部分:
在mainPage.vue要替换的地方加router-view标签,将
Main
改为:
然后配置路由时,配成子路由的形式:
{path:'/main',component:Main,children:[ {path:'/users',component:Users}, {path:'/menus',component:Menus} ]},
子路由就是来配合替换部分页面的。
此时效果:
④.加访问权限控制
后端有权限控制,前端却没有。
将后台服务器重启,登录信息没了。前端服务器访问菜单模块,后端返回响应,没有登录。
但前台可以随便敲,虽然后台数据不会给。
没有登录,应该返回登录页面,这样和后台就一致了。
在mainPage组件里:
.then((ret)=>{ console.log(ret); if(ret.data.returnCode==10000){ this.menuList = ret.data.returnData; }else if(ret.data.returnCode==20000){ this.$router.push("/login"); } })
虽然现在是两个服务器了,但前端服务常配合后台服务接口做一些功能。
但是不是所有的请求都要加else if这样一个处理?即其他组件发ajax请求时是否也要看下有没有登录,做对应的处理。故还是找个公共的方式,axios官网提供了。
现在需要响应拦截器:
回到入口文件main.js:
import Vue from 'vue' import App from './App.vue' import router from './router' import './plugins/element.js' import qs from 'qs' import axios from 'axios' Vue.config.productionTip = false /*为axios配置全局参数*/ //服务器路径 axios.defaults.baseURL="http://localhost:8080/" //跨域带cookie axios.defaults.withCredentials = true //axios响应拦截器 配合后台登录访问控制 axios.interceptors.response.use(function (response) { //如果没有登录 returnCode==20000 if(response.data.returnCode==20000){ //跳转到登录组件 router.push("/login"); } return response; }, function (error) { // 对响应错误做点什么 return Promise.reject(error); }); Vue.prototype.$qs = qs; Vue.prototype.$axios = axios new Vue({ router, render: h => h(App) }).$mount('#app')
此时可删除其他组件发送请求失败时的else if。
至此,将系统里的公共功能写完了,每套系统以上部分都差不多。
接下来就写具体模块的增删改查。
vue工具下如何处理增删改查。
在menus组件加代码。
刚进该组件,应该先查现有的菜单信息。
1.3.1 查询功能
①.elementUI将表格拉过来:
菜单信息需要展示出来,故将table拉进来(之前讲过)。
(把代码复制一份)
②.绑定服务接口
要绑定的服务接口是MenuServlet(其配置地址是menu)
先发Ajax请求,并打印出返回的数据,看一下:
(参数可先不给,默认第一页,每页10条记录)
mounted(){ console.log("页面加载结束后执行"); this.$axios.get("/menu/query") .then((ret)=>{ console.log(ret); }).catch((err)=>{ console.log(err); }); }
返回的数据如下:
所有的后台数据都有响应码,前端处理时根据响应码做一下区分。
若响应码是10000,做数据解析。
mounted(){ console.log("页面加载结束后执行"); this.$axios.get("/menu/query") .then((ret)=>{ /* 从返回的数据复制一份 data: pageinfo: {page: 1, pagesize: 10, total: 29} returnCode: 10000 returnData: Array(10) 0: {glyphicon: 'el-icon-user', menuid: 11, menuname: '管理中心', menuurl: '#', pid: 0, …} 1: {glyphicon: 'el-icon-s-order', menuid: 12, menuname: '模版管理', menuurl: '#', pid: 0, …} 2: {glyphicon: 'el-icon-data-line', menuid: 13, menuname: '广告管理', menuurl: '#', pid: 0, …} 3: {glyphicon: 'el-icon-timer', menuid: 14, menuname: '游戏管理', menuurl: '#', pid: 0, …} 4: {glyphicon: 'el-icon-ship', menuid: 15, menuname: '合作公司管理', menuurl: '#', pid: 0, …} 5: {glyphicon: 'el-icon-setting', menuid: 16, menuname: '渠道版本', menuurl: '#', pid: 0, …} 6: {glyphicon: 'el-icon-setting', menuid: 17, menuname: 'CP', menuurl: '#', pid: 0, …} 7: {glyphicon: 'el-icon-setting', menuid: 18, menuname: '图书管理', menuurl: '#', pid: 0, …} 8: {glyphicon: 'el-icon-setting', menuid: 19, menuname: '标签管理', menuurl: '#', pid: 0, …} 9: {glyphicon: 'el-icon-setting', menuid: 20, menuname: '分类管理', menuurl: '#', pid: 0, …} */ console.log(ret); if(ret.data.returnCode==10000){ this.tableData = ret.data.returnData; } }).catch((err)=>{ console.log(err); }); }
假数据前面已经测试过,页面正常显示,先删除假数据:
data() { return { tableData: [] }; }
③.根据后台返回的字段修改el-table的数据:
会发现返回的数据多了个父级菜单的名字pname字段。数据库里只有pid,但前端最终展示给用户看,用父级菜单更合适。上级菜单的名称查询如下:
对应的sql:
把后台返回的字段在页面里修改一下:
效果如下:
图标直接来个文本不好看。
上面不是自己加的v-for,他是自动遍历的,故table里想自己加数据,需要用到数据插头。
显示一个图标,再显示一个文本:
{{mydata.row.glyphicon}}
此时的代码:
{{mydata.row.glyphicon}}
④.分页功能
从elementUI的Data找分页组件,复制一份过来,复制到el-table下面:
将:current-page="currentPage4"先改成:current-page="1"
函数handleSizeChange页码改变和handleCurrentChange每页显示多少条改变的对应函数也拉进来。
methods: { handleSizeChange(val) { console.log(`每页 ${val} 条`); }, handleCurrentChange(val) { console.log(`当前页: ${val}`); } },
template标签里只能有一个分标签,故template里面最外层套一个div。
现在需要将分页组件和表格里的数据建立关系:
每次查询,后端发过来的数据都会有pageinfo
需要将pageinfo和分页组件的相关数据搭配起来:
先在data里建一个pageinfo对象,顺便做一组假数据:
data() { return { tableData: [], /* pageinfo: page: 1 pagesize: 10 total: 29 */ pageinfo:{ page: 3, pagesize: 10, total: 660 } }; }
再将拉进来的分页插件做相应修改:
发Ajax请求时,页码相关数据传给分页插件:
(使用json对象的另一个好处,如果返回的键值对和设置的json对象的键值对完全一样,整个对象给他就行,不需要一个一个值去给)
if(ret.data.returnCode==10000){ //展示table数据 this.tableData = ret.data.returnData; //把页码相关数据传给分页组件 //this.pageinfo = ret.data.pageinfo; }
现在组件按真数据走,但翻页的功能还要改。翻页就是往后端传要看第几页,每页显示多少条。
handleCurrentChange(val) { //页数发生改变触发该函数 console.log(`当前页: ${val}`); //可知当前要翻的页数 }
只需再传查询的Ajax即可,上面最开始就是查询,多个查询可合并成一个。
故再新建个查询函数,其他的调用它:
methods: { handleSizeChange(val) { console.log(`每页 ${val} 条`); }, handleCurrentChange(val) { //页数发生改变触发该函数 console.log(`当前页: ${val}`); //可知当前要翻的页数 }, myQuery(params){ this.$axios.get("/menu/query?"+params) //get方式可直接拼参数 .then((ret)=>{ /* 从返回的数据复制一份 data: pageinfo: {page: 1, pagesize: 10, total: 29} returnCode: 10000 returnData: Array(10) 0: {glyphicon: 'el-icon-user', menuid: 11, menuname: '管理中心', menuurl: '#', pid: 0, …} 1: {glyphicon: 'el-icon-s-order', menuid: 12, menuname: '模版管理', menuurl: '#', pid: 0, …} 2: {glyphicon: 'el-icon-data-line', menuid: 13, menuname: '广告管理', menuurl: '#', pid: 0, …} 3: {glyphicon: 'el-icon-timer', menuid: 14, menuname: '游戏管理', menuurl: '#', pid: 0, …} 4: {glyphicon: 'el-icon-ship', menuid: 15, menuname: '合作公司管理', menuurl: '#', pid: 0, …} 5: {glyphicon: 'el-icon-setting', menuid: 16, menuname: '渠道版本', menuurl: '#', pid: 0, …} 6: {glyphicon: 'el-icon-setting', menuid: 17, menuname: 'CP', menuurl: '#', pid: 0, …} 7: {glyphicon: 'el-icon-setting', menuid: 18, menuname: '图书管理', menuurl: '#', pid: 0, …} 8: {glyphicon: 'el-icon-setting', menuid: 19, menuname: '标签管理', menuurl: '#', pid: 0, …} 9: {glyphicon: 'el-icon-setting', menuid: 20, menuname: '分类管理', menuurl: '#', pid: 0, …} */ console.log(ret); if(ret.data.returnCode==10000){ //展示table数据 this.tableData = ret.data.returnData; //把页码相关数据传给分页组件 this.pageinfo = ret.data.pageinfo; } }).catch((err)=>{ console.log(err); }); } }, mounted(){ console.log("页面加载结束后执行"); let params = "" this.myQuery(params); //包含在vue对象里,直接调用即可。初次查询没有参数,给一个空值 }
现在处理翻页:
handleCurrentChange(val) { //页数发生改变触发该函数 console.log(`当前页: ${val}`); //可知当前要翻的页数 this.pageinfo.page = val; //将当前组件的page值设置成要翻页的页码 let params = this.$qs.stringify(this.pageinfo); //传一个pageinfo对象给后台,后端拿需要的参数即可 console.log(params); //先看一下这个参数 }
点击第二页,触发该函数,控制台输出:
参数没问题。
开始发参数:
handleCurrentChange(val) { //页数发生改变触发该函数 console.log(`当前页: ${val}`); //可知当前要翻的页数 this.pageinfo.page = val; //将当前组件的page值设置成要翻页的页码 let params = this.$qs.stringify(this.pageinfo); //传一个pageinfo对象给后台,后端拿需要的参数即可 console.log(params); //先看一下这个参数 this.myQuery(params); //要加this,不加this就找外部的全局参数。加this找vue里methods定义的函数。 }
现在翻页和跳页都正常了。
下拉列表里是页码数,点击20条/页,现在没反应,也需要传值查数据。
handleSizeChange(val) { console.log(`每页 ${val} 条`); this.pageinfo.pagesize = val; //选中每页需要展示的页码数 this.pageinfo.page = 1; //切换pagesize之后,习惯将页数指定到第一页 let params = this.$qs.stringify(this.pageinfo); console.log(params); this.myQuery(params); }
至此,翻页功能做完。
现在全部的代码:
{{mydata.row.glyphicon}}
页面效果:
⑤.条件查询
后端MenuServlet条件查询需要接收的参数是qmname和qpid。故前端应该把这两个值传到后台。
name部分模糊查询,id部分精确查询。
到elementUI拉一个查询按钮和两个框。在Form表单部分。
复制的内容放到el-table上面:
查询
修改内容:
绑定queryForm对象,queryForm传的两个值和后台搭配起来。
查询
将对象创建出来:
queryForm:{ qmname:'', //做双向绑定时,就不给假数据了,默认空值 qpid:'' }
将查询函数写出来:
queryMenu(){ let params = this.$qs.stringify(this.queryForm); this.myQuery(params); }
测试:
模糊查询:
再翻翻页(翻页也带条件)
翻页的时候,参数多添加一些:
handleSizeChange(val) { console.log(`每页 ${val} 条`); this.pageinfo.pagesize = val; //选中每页需要展示的页码数 this.pageinfo.page = 1; //切换pagesize之后,习惯将页数指定到第一页 let params = this.$qs.stringify(this.pageinfo)+"&"+this.$qs.stringify(this.queryForm); console.log(params); this.myQuery(params); }, handleCurrentChange(val) { //页数发生改变触发该函数 console.log(`当前页: ${val}`); //可知当前要翻的页数 this.pageinfo.page = val; //将当前组件的page值设置成要翻页的页码 let params = this.$qs.stringify(this.pageinfo)+"&"+this.$qs.stringify(this.queryForm); ; //传一个 pageinfo对象给后台,后端拿需要的参数即可 console.log(params); //先看一下这个参数 this.myQuery(params); //要加this,不加this就找外部的全局参数。 }
再测,翻页时也带上了条件。
精确查询:
再测个父菜单0的,并且翻翻页。
再测菜单名称:管理234,父菜单:0。找个没数据的。
⑥.查询改成下拉列表
两个输入框使用不方便。将数据库里父id有的数据放在父菜单做成下拉列表。
还是刚才Form表单的那个例子,将相关代码复制进来:
查询
效果如下:
无是特殊的一个。不需要从后台拿。
下拉列表的相关数据从后台获取:
对应的在MenuServlet里有getmenuselect方法。
在postman里测一下:
将菜单数据的方法定义下:
将拿到的数据传给selectOptions
getMenuSelect(){ this.$axios.get("/menu/getmenuselect") .then((ret)=>{ console.log(ret); if(ret.data.returnCode==10000){ //展示table数据 this.selectOptions = ret.data.returnData; } }).catch((err)=>{ console.log(err); }); } }
相应的data里增加:
selectOptions:[],
回到下拉列表,需要增加的选项用v-for从selectOptions里遍历拿数据。
label里写名称(给用户看),value里写值(往后台做查询用)。
查询
页面加载结束后,拿下拉列表的数据:
mounted(){ console.log("页面加载结束后执行"); let params = ""; this.myQuery(params); this.getMenuSelect(); }
⑦.重置查询表单
增加一个重置按钮,清空el-form表单数据。
首先给el-form加ref属性,将组件注册上去。
然后加一个点击事件,将注册后的名字以参数形式传入。
查询 重置
增加重置函数:
resetForm(formname){ this.$refs[formname].resetFields(); }
form-item上需要有prop属性:
查询 重置
查询功能结束,代码如下:
查询 重置 {{mydata.row.glyphicon}}
效果如下:
1.3.2添加功能
增加添加按钮,添加信息时弹出弹出框。
①.elementUI拉过弹出框
在Others的Dialog,复制相关代码:
放到div里的最下层:
打开嵌套表单的 Dialog
按钮还是放上面:
换一下添加按钮的样式
查询 重置 打开嵌套表单的 Dialog
关键的值在data里定义一下:
dialogFormVisible : false, formLabelWidth: '120px'
新拉进来的代码还绑定了form,故在data里,定义绑定的数据:(改成addForm)
addForm:{ myval1:'', myval2:'' }
上面的数据将form也全部改成addForm,并修改相应参数:
dialogFormVisible太长,以上全部换成addFormVisible
将添加按钮的内容改成添加
添加
设置弹出框的内容:
父id应该做成下拉列表。
将假数据改成真数据:
/* String menuid = req.getParameter("menuid"); String menuname = req.getParameter("menuname"); String menuurl = req.getParameter("menuurl"); String pid = req.getParameter("pid"); String glyphicon = req.getParameter("glyphicon"); */ addForm:{ menuid:'', menuname:'', menuurl:'', pid:'', glyphicon:'', }
效果如下:
修改父菜单样式:
同时在样式标签里:
.el-select{ width:100%; }
点击确定后,应往后台发送,也做成一个函数:
确 定
对应函数:
addFormSubmit(){ this.addFormVisible = false; //让弹出框关闭 //this.$qs.stringify(this.addForm); //取addForm数据,并格式化 this.$axios.post("/menu/add",this.$qs.stringify(this.addForm)) .then((ret)=>{ //先和接口接通 }).catch((err)=>{ }) }
随便写一组数据,点击确定,数据库添加成功。
②.做出弹出框后,添加数据结束,使页面相应改变
添加请求成功,但页面没反应。故干两个事:
1.数据刷新(实际是重新查询一遍) 2.打印出执行操作的结果(用提示框)。
接收到后台的响应代码后,做出不同的提示框(代码从elementUI取)。
this.$axios.post("/menu/add",this.$qs.stringify(this.addForm)) .then((ret)=>{ //先和接口接通 //根据响应 提示操作的结果 if(ret.data.returnCode==10000){ this.$message(ret.data.returnMsg); }else{ this.$message.error(ret.data.returnMsg); } }).catch((err)=>{ })
刷新数据:
this.$axios.post("/menu/add",this.$qs.stringify(this.addForm)) .then((ret)=>{ //先和接口接通 //根据响应 提示操作的结果 if(ret.data.returnCode==10000){ this.$message({ message:ret.data.returnMsg, type: 'success' }); }else{ this.$message.error(ret.data.returnMsg); } //刷新数据 let params = this.$qs.stringify(this.pageinfo)+"&"+this.$qs.stringify(this.queryForm); this.myQuery(params); }).catch((err)=>{ })
③.一个问题:添加完成以后,父菜单的下拉列表没有跟着动。
因为该下拉列表是页面加载结束取的。添加完数据没有再取数据。故刷新数据也要刷新父菜单的下拉列表数据:
this.$axios.post("/menu/add",this.$qs.stringify(this.addForm)) .then((ret)=>{ //先和接口接通 //根据响应 提示操作的结果 if(ret.data.returnCode==10000){ this.$message({ message:ret.data.returnMsg, type: 'success' }); }else{ this.$message.error(ret.data.returnMsg); } //刷新数据 let params = this.$qs.stringify(this.pageinfo)+"&"+this.$qs.stringify(this.queryForm); this.myQuery(params); //刷新下拉列表数据: this.getMenuSelect(); }).catch((err)=>{ })
添加结束后,再添加数据时,上一次的数据还在。应该再次点击添加按钮时,弹出框的数据清空。
找到添加按钮:
添加
找到表格,注册一下addForm,并给下面的表格项增加prop属性:
在下面定义该函数:
addFormOpen(formName){ this.addFormVisible = true; this.$refs[formName].resetFields(); }
④.功能正常,但还报错
虽然可以正常运行,但这里报错
意思是this.$refs[formName]是空的,再拿空对象调resetFields()。类似java里的空指针异常。
组件已经注册上去,怎么会空?
这是vue机制问题,它的渲染过程是自动的,交给它的代码的执行顺序不一定是我们编写的代码的顺序。
使用这个对象时,组件还没注册,故报错。
解决办法:
addFormOpen(formName){ this.addFormVisible = true; this.$nextTick(function(){ //渲染时机后移,做完一次渲染后再执行括号里的代码 this.$refs[formName].resetFields(); }) }
增加功能结束,代码如下:
查询 重置 添加 {{mydata.row.glyphicon}}
1.3.3修改功能
①.定位当前行数据
法一:每一行记录加一个修改按钮
法二:使table变成可选择的效果
这里用法二,elementUI的Table表格,复制相关代码(单选):
Table 组件提供了单选的支持,只需要配置highlight-current-row
属性即可实现单选。之后由current-change
事件来管理选中时触发的事件,它会传入currentRow
,oldCurrentRow
。如果需要显示索引,可以增加一列el-table-column
,设置type
属性为index
即可显示从 1 开始的索引号。
在页面显示的table标签上:
定义该方法:
该方法名被翻页占了,改成selectRowChange
selectRowChange(val){ //参数是当前改变的这行数据 this.currentRow = val; }
data里定义该对象:
currentRow: null
按钮那块没传参,改成如下,就是用它的默认参数:
@current-change="handleCurrentChange"
现在行就选中了。再加一个按钮:
修改
②.将选中行数据填到修改框里
editForm和添加很像。
直接复制一份addForm,做修改即可。
菜单编号不让修改,只读。
下面对应的数据:
将data里addForm复制一份修改:
editForm:{ menuid:'', menuname:'', menuurl:'', pid:'', glyphicon:'', }, editFormVisible : false,
点击修改按钮,先让它的弹出框出来:
editFormOpen(){ this.editFormVisible = true; }
接下来将选中的数据填到弹出框:
即将选中的数据填到editForm字段里:
editFormOpen(){ this.editFormVisible = true; this.editForm = this.currentRow; //将选中行的数据对象填到editForm里 }
选中不同的行,修改框里的父菜单内容有点问题:
这是数据类型的问题:
select里,0是自己拼的。其他的值是动态生成的。
使用value数据绑定时,后台给的格式和这个格式对应,现在使用menuid实际是数字类型。这个选项和它的类型没有匹配上。
这里加个冒号,变成数值绑定,里面写的js代码,0就是数字0.(第一天讲value时的问题)
③.修改数据
修改数据,还没传到后台,页面数据就改变了。
这也是vue里的一个机制,vue会把这些对象监控起来
里面有一个ob,即Observer。表示对象被监控了,并且每个对象会起一个对应的对象编号。这个对象是遍历数据时生成的。
editFormOpen(){ this.editFormVisible = true; //将选中行的数据对象填到editForm里 //this.editForm = this.currentRow; //这里实际只传了对象的引用,故editForm里的数据修改,页面数据也会改。他们指向同一个引用,并且做了数据双向绑定 //java里也有同样的问题,深克隆和浅克隆 //console.log(JSON.stringify(this.currentRow)); //json格式字符串化,且是标准的json格式 //console.log(JSON.parse(JSON.stringify(this.currentRow))); //将对象转成字符串,再转回来,就新创建了一个对象,虽然数据还是一样的 //this.editForm = this.currentRow; //用新的对象赋值给editForm。这就是js里的深克隆 this.editForm = JSON.parse(JSON.stringify(this.currentRow)); //可写成一句话 }
接下来点击修改弹出框的确定按钮,将数据从editFrom里拿到,传给后台,后台保存起来。
editFormSubmit函数类似addFormSubmit:
editFormSubmit(){ this.editFormVisible = false; //让弹出框关闭 this.$axios.post("/menu/edit",this.$qs.stringify(this.editForm)) .then((ret)=>{ if(ret.data.returnCode==10000){ this.$message({ message:ret.data.returnMsg, type: 'success' }); }else{ this.$message.error(ret.data.returnMsg); } //刷新数据 let params = this.$qs.stringify(this.pageinfo)+"&"+this.$qs.stringify(this.queryForm); this.myQuery(params); //刷新下拉列表数据: this.getMenuSelect(); }).catch((err)=>{ }) }
除了参数不同,都一致。
再加一个函数,写公共代码:
myCUD(url,params){ this.$axios.post(url,params) .then((ret)=>{ //先和接口接通 //根据响应 提示操作的结果 if(ret.data.returnCode==10000){ this.$message({ message:ret.data.returnMsg, type: 'success' }); }else{ this.$message.error(ret.data.returnMsg); } //刷新数据 let params = this.$qs.stringify(this.pageinfo)+"&"+this.$qs.stringify(this.queryForm); this.myQuery(params); //刷新下拉列表数据: this.getMenuSelect(); }).catch((err)=>{ }) }
修改:
editFormSubmit(){ this.editFormVisible = false; //让弹出框关闭 let url = "/menu/edit"; let params = this.$qs.stringify(this.editForm); this.myCUD(url,params); }
添加:
addFormSubmit(){ this.addFormVisible = false; //让弹出框关闭 let url = "/menu/add"; let params = this.$qs.stringify(this.addForm); this.myCUD(url,params); }
此时,整体代码如下:
查询 重置 添加 修改 {{mydata.row.glyphicon}}
效果如下:
1.3.4删除功能
定位一条数据(已有),并做一个删除数据的展示(不用弹出框)。
①.复制小弹出框的代码
elementUI里的Notice里的MessageBox的确认信息.
添加进按钮:
删除
将removeMenu()方法添加:
removeMenu(){ this.$confirm('此操作将删除编号为 '+this.currentRow.menuid+' 的记录, 是否继续?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { //表示成功,即点击了确定按钮 this.$message({ type: 'success', message: '删除成功!' }); }).catch(() => { //表示失败 this.$message({ type: 'info', message: '已取消删除' }); }); }
点击确定按钮后的内容自己改下:
应该将要删除的id传到后台的删除接口
删除完还是刷新对应的数据。
removeMenu(){ this.$confirm('此操作将删除编号为 '+this.currentRow.menuid+' 的记录, 是否继续?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { //表示成功,即点击了确定按钮 //把id发到后台删除 console.log(this.currentRow.menuid); let url = "/menu/remove"; let params = "menuid="+this.currentRow.menuid; this.myCUD(url,params); }).catch(() => { //表示失败 this.$message({ type: 'info', message: '已取消删除' }); }); }
单个模块的增删改查就写完了。
代码如下:
查询 重置 添加 修改 删除 {{mydata.row.glyphicon}}
1.3.5最后一个功能
修改和删除按钮,只有选中记录才会生效。不选中直接点会报错,这里优化一下。
没选中记录,这两个按钮禁用。
这两个按钮的disabled属性本来是true,点击一行之后变成false。
需要属性绑定的方式去写。
先加两个disabled的属性:
修改 删除
在data里增加:
btnstatus:true
点击一行能用,找selectRowChange函数,
selectRowChange(val){ //参数是当前改变的这行数据 this.currentRow = val; this.btnstatus = false; }
使用vue之后,如何考虑页面效果的切换。做页面绑定。
现在翻页之后,按钮还可以选择,应该变回true。选择每页的记录数后,也应该变为true。
也就是凡是页面重新查询后,按钮都该禁用。
myQuery(params){ this.$axios.get("/menu/query?"+params) .then((ret)=>{ console.log(ret); if(ret.data.returnCode==10000){ this.btnstatus = true; //按钮禁用 //展示table数据 this.tableData = ret.data.returnData; //把页码相关数据传给分页组件 this.pageinfo = ret.data.pageinfo; } }).catch((err)=>{ console.log(err); }); }
好像不太好用,有时候可行,有时候不行。
还是渲染时机问题。
this.$nextTick(function(){ this.btnstatus = true; //按钮禁用 })
现在不好使了。
因为nextTick只是把渲染时机后移,具体移到哪次渲染之后没法控制。
现在使用vue的监控机制,可精确找到代码要执行的时机。
数据刷新时,会取消掉选中的行,也就是table重新生成的时候。
watch:{ //监控指定的数据 tableData:function(){ //刷新数据后自动触发 console.log("tableData数据渲染完成了"); this.$nextTick(function(){ this.btnstatus = true; //按钮禁用 }) } }
和mounted(钩子函数)是同级的。
最终的代码:
查询 重置 添加 修改 删除 {{mydata.row.glyphicon}}