SSM项目实战前端开发

通过前面后端的开发,接下来我们来操作前端开发(大致说明一下,不会说明全部)
且要注意,这里会与前面博客中后端项目结合起来
Vue回顾:
项目结构说明:
我们使用脚手架快速构建Vue项目,项目结构如下图:
对应的前端部分项目地址(只要部分,即下面我们来补充与后端的连接):
链接:https://pan.baidu.com/s/1o0yyu9gOtOtwwcaDzsnYDQ
提取码:alsk
注意:我们先不要运行,因为是需要登录的,等后面说明了登录才进行运行,当然也可以进行登录
基本可以随便登录的(因为对应的操作被注释了),且设置了固定值,所以基本可以随便登录,后面会进行改变
部分目录和解释:

Views 目录说明:
我们来一起看一下,前端项目的页面部分:

CourseManage:课程管理
AdvertiseManage:广告管理
PermissionManage:权限管理
CommentManage:公共
Users.vue:用户管理
Login.vue:登录
vue组件化开发:
每一个*.vue 文件都可以看做是一个组件
组件的组成部分:
template:组件的HTML部分
script:组件的JS脚本 (使用ES6语法编写)
style:组件的CSS样式
<!-- 1.template 代表html结构, template中的内容必须有且只有一个根元素编写页面静态部分 就是 view部分 --><template><div>测试页面...</div>
</template>
<!-- 2.编写vue.js代码 -->
<script>//可以导入其组件// import Header from '../components/header.vue' //默认写法, 输出该组件信息,那么自然会用到,在vue里面相当于new Vue({}),即可以使用对应vue语法export default {name:"Home", // 组件名称,用于以后路由跳转data() {// 当前组件中需要使用的数据return {}},methods: {}}
</script>
<!-- 编写当前组件的样式代码 -->
<style scoped>/* 页面样式 加上scoped 表示样式就只在当前组件有效*/
</style>
课程模块回顾:
注意:下面代码是项目里的主要代码
功能分析:
Course.vue 组件(改变名称后),完成课程数据的展示和条件查询
使用ElementUI 表格进行数据展示
https://element.eleme.cn/#/zh-CN/component/table
部分最开始请求:
//获取课程数据loadCourses() {this.loading = true;const data = {};//js的is中如果是0,null,空串,undifined则相当于false,除此之外则相当于trueif (this.filter.courseName) data.courseName = this.filter.courseName;if (this.filter.status) data.status = this.filter.status;//发送请求return axios.post("/course/findAllCourse", data).then(resp => {this.courses = resp.data.content;this.loading = false;}).catch(error => {this.$message.error("数据获取失败! ! !");});}

prop可以说是绑定数据的对应属性名数据
对应js代码:
//数据部分data() {//查询条件const filter = {courseName: "",status: ""};return {filter,courses: [], //课程数据loading: false};},//钩子函数created() {this.loadCourses();}
条件查询(访问的是同一个接口 ):
<el-button @click="handleFilter">查询</el-button>
//条件查询handleFilter() {this.loadCourses();}
新建课程 :
功能分析:
点击新建,由路由导航到 CourseItem.vue
<el-button type="primary" icon="el-icon-plus" @click="handleAdd">新建课程</el-button>
//新建课程 路由跳转
handleAdd() {this.$router.push({ name: "CourseItem", params: { courseId: "new" } });//对应路由的参数,必须由params属性操作
}
router.js:
 {path: "/courses/:courseId",name: "CourseItem",meta: { requireAuth: true, title: "课程详情" },component: () =>import(/* webpackChunkName: 'courses' */ "../views/CourseManage/CourseItem.vue")//使用import可以不用我们来使用import导入了,而直接使用}
CourseItem组件使用ElementUI中的表单来提交课程数据:
https://element.eleme.cn/#/zh-CN/component/form

JS代码编写 :
<el-button type="primary" @click="handleSave">保存</el-button>
//保存修改课程信息handleSave() {//校验是否符合规则this.$refs.form.validate(valid => {if (!valid) return false;axios.post("/course/saveOrUpdateCourse", this.course).then(res => {//退回到上个页面this.$router.back();}).catch(error => {this.$message.error("保存课程信息失败! ! !");});});},
课程图片上传:
案例演示:
我们创建一个Vue项目来演示图片上传组件的使用方式
首先要有一个项目:
代码地址如下:
链接:https://pan.baidu.com/s/1PsQRmqdbr3Jj2cbr2X6pcQ
提取码:alsk
我们用这个项目来操作测试
注意上传图片是有要求的,因为目标的服务器(这里是)只会接收符合他条件的图片,如果上传不了,可以上传其他图片
在测试项目的components目录下创建一个 UploadImage.vue组件:
查看ElementUI文档,复制代码到 UploadImage.vue
https://element.eleme.cn/#/zh-CN/component/upload
<template><div><el-uploadaction="https://jsonplaceholder.typicode.com/posts/"list-type="picture-card":on-preview="handlePictureCardPreview":on-remove="handleRemove" ><!--:on-remove="handleRemove"是移除时操作指定的运行方法--><i class="el-icon-plus"></i></el-upload><el-dialog :visible.sync="dialogVisible"><img width="100%" :src="dialogImageUrl" alt="" /></el-dialog></div>
</template>
<script>
export default {data() {return {dialogImageUrl: "",dialogVisible: false,};},methods: {handleRemove(file, fileList) {console.log(file, fileList);},handlePictureCardPreview(file) {this.dialogImageUrl = file.url;this.dialogVisible = true;},},
};
</script>
然后在components目录下的index.vue修改或者添加(也是一个操作路由的):
 <el-submenu index="1"><template slot="title"><i class="el-icon-location"></i><span>导航菜单</span></template> <el-menu-item-group><!-- 修改 index的路由地址 --><el-menu-item index="/upload"><i class="el-icon-menu"></i>图片上传</el-menu-item></el-menu-item-group></el-submenu>
配置路由(操作router目录下的index.js):
 children: [{path: "/upload",name:"upload",component: () => import("@/components/UploadImage"),}]
最后可以执行操作了,即访问页面进行测试
属性说明 (了解即可):

上面的action是将图片上传到那里去
组件的引入:
怎么将一个组件引入另一个组件
接下来我们来演示一下 引入图片组件(实际上是根据import导入后来注册使用组件来操作的,这里是局部组件)
在测试项目components目录下创建一个TestUplopad.vue组件:
<template><!--使用组件--><upload-image></upload-image>
</template>
<script>
import UploadImage from '@/components/UploadImage'
export default {  //export default可以理解为可以在给自己使用的同时,也可以给被导入使用//注册局部组件,可以给当前组件里使用组件UploadImage,相当于全局操作了components: { UploadImage },}
</script>
<style scoped></style>
然后在对应的index.vue中加上:
   <el-menu-item-group><!-- 修改 index的路由地址 --><el-menu-item index="/text"><i class="el-icon-menu"></i>测试组件的引入</el-menu-item></el-menu-item-group>
<!--当作路由-->
加上对应路由(index.js):
  {path: "/text",name:"text",component: () => import("@/components/TextUpload"),}
然后就可以进行测试了
对应的项目执行流程可以参照58章博客
组件的传参 :
UploadImage.vue组件的部分修改
data() {return {dialogImageUrl: "",dialogVisible: false,actionUrl:this.uploadUrl //传递的参数要接收};},/*props组件传参,给组件添加参数,为uploadUrluploadUrl当作图片上传路径https://jsonplaceholder.typicode.com/posts/getUrl:函数*/props:["uploadUrl","getUrl"],
<div><!--
:action="actionUrl" 使用data的值,但是参数时,加上:
--><el-upload:action="actionUrl" list-type="picture-card":on-preview="handlePictureCardPreview":on-remove="handleRemove":on-success="successUpload"><!--上传成功后,调用successUpload方法-->
对应方法:
 successUpload(response, file){this.getUrl(file);}
对应操作导入上面组件的TestUplopad.vue组件的部分修改:
 <upload-image uploadUrl="https://jsonplaceholder.typicode.com/posts/" :get-url="show"></upload-image>
<!--设置添加了uploadUrl参数-->
<!--传递了show方法过去-->
对应方法:
  methods:{show(file){console.log("show方法被调用了")console.log(file);}}
//即我们执行geturl也就是:get-url的值是show方法,所以执行geturl(file)也就是执行show(file)方法
接下来进行测试了
我们通过上面的测试操作,接下来再看看如下操作
课程模块图片上传
我们可以看看对应的CourseItem.vue组件的图片上传代码
  <!-- 使用图片上传组件,完成图片上传 --><upload-image:content="course.courseImgUrl && [course.courseImgUrl]":get-urls="getCourseImgUrl"uploadUrl="/course/courseUpload"ref="courseCoverRef"max="10M"tipInfo="建议尺寸:230*300px,JPG、PNG格式,图片小于10M"></upload-image><!--发现这也是对应的组件,且设置添加了参数-->
<!--那么对应的js也应该引入了对应组件的-->
对应部分js如下
import UploadImage from "@/components/UploadImage.vue";
对应的部分UploadImage.vue组件:
 <el-uploadclass="upload-demo":action="uploadAction"  :multiple="false":limit="1":before-upload="beforeUpload":on-success="uploadSuccess":on-remove="removeSuccess":on-exceed="exceedFile":file-list="fileList"list-type="picture-card"><!-- :action="uploadAction" 也是对应值路径-->
对应的js
data() {return {//uploadUrl= /course/courseUpload//process.env.VUE_APP_API_BASE全局的值,这里也就是路径uploadAction: process.env.VUE_APP_API_BASE + this.uploadUrl, //封装了路径来使用fileList: [],data: {}};},
//js中可以再属性里加注释,而html在属性里不可加,因为html标签的原因/*** 组件传参*    content 图片地址*    get-urls 获取图片地址*/props: ["content", "getUrls", "max", "tipInfo", "uploadUrl"],
发现也是一样的操作
注意:只要不是写死的资源,那么基本都会在检查里中的网络发现访问(注意是全部请求)
但是该访问是自己到这个页面的访问,而在该页面其中操作时,不会显示出来
ctrl+d可以查看当前页面代码选中的其他代码,一般从上到下查找,可以多次按
修改课程 :
CourseItem.vue组件部分代码
 axios.post("/course/saveOrUpdateCourse", this.course).then(res => {//退回到上个页面this.$router.back();}).catch(error => {this.$message.error("保存课程信息失败! ! !");});
修改课程与添加课程走的都是同一个后台接口,区别是修改操作必须要携带ID
在这个Typora中按ctrl+.(点)可以切换是否可以输入中文符号,一般默认可以输入
课程状态管理 (Course.vue组件):
点击上架或者下架完成课程状态的切换
部分代码:
 axios.get("/course/updateCourseStatus", {params: {status: toggledStatus,id: item.id}}).then(res => {debugger;//设置最新的值item.status = toggledStatus;console.log(item);//重新加载页面window.location.reload;}).catch(error => {this.$message.error("状态修改失败! ! !");});
课程内容管理 :
获取课程内容数据:
课程内容数据包括章节与课时信息,根据课程ID 查询课程包含的章节与课时信息
CourseSections.vue组件部分代码:
 axios.get("/courseContent/findCourseByCourseId?courseId=" + id).then(res => {const course = res.data.content;//将数据保存到章节表单对象中this.addSectionForm.courseId = course.id;this.addSectionForm.courseName = course.courseName;//将数据保存到课时表单对象中this.addLessonForm.courseId = course.id;this.addLessonForm.courseName = course.courseName;}).catch(error => {this.$message.error("数据获取失败! ! !");});
//注意:这里的后台好像需要改变一下,使得不返回空数据,使得有默认,而显示在前端//且要记住,有些字段是不能为空的,也就是说,若不写对应字段的话一般就会报错,除非你设置了初始值
章节管理:
新建和修改章节:
CourseSections.vue组件部分代码
 axios.post("/courseContent/saveOrUpdateSection", this.addSectionForm).then(res => {this.showAddSection = false;//重新加载列表return this.loadSections(this.addSectionForm.courseId);}).then(() => {//重置表单内容this.addSectionForm.sectionName = "";this.addSectionForm.description = "";this.addSectionForm.orderNum = 0;this.reload();}).catch(error => {this.showAddSection = false;this.$message.error("操作执行失败! ! !");});
添加与修改章节访问的都是同一个接口
章节状态:
CourseSections.vue组件部分代码
 axios.get("/courseContent/updateSectionStatus", {params: {id: this.toggleStatusForm.id,status: this.toggleStatusForm.status}}).then(resp => {this.toggleStatusForm.data.status = this.toggleStatusForm.status;this.toggleStatusForm = {};this.showStatusForm = false;this.reload();}).catch(error => {this.showStatusForm = false;this.$message.error("修改状态失败! ! !");});
课时管理基本与章节类似,也就不操作了
大致回顾完毕,接下来我们来操作不一样的
上面是回顾,所以对应登录时可以操作,下面才是真正要操作的地方,所以当你没有编写下面代码,基本是访问不了的
广告模块:
广告位管理:
广告位展示 :
AdvertiseSpaces.vue 组件,为广告位页面
我们需要在其前端项目上进行添加访问后端的代码:
对应的js部分:
 //方法1: 加载广告位信息loadPromotionSpace() {//开启加载画面this.listLoading = true;//请求后台接口 获取广告位列表数据axios.get("/PromotionSpace/PromotionSpace").then(res => {this.list = res.data.content;//关闭加载画面this.listLoading = false;}).catch(err => {this.$message.error("加载广告位列表数据失败");});},
添加广告位:
在AdvertiseSpaces.vue 组件里有:
 <el-button size="mini" class="btn-add" @click="handleAdd()">添加广告位</el-button>
进行跳转:
对应的js部分:
//添加广告位跳转handleAdd() {this.$router.push({ path: "/addAdvertiseSpace" });},
查看部分路由routes.js(不要认为非常复杂,我们可以将路由看成将vue组件指定显示地方的操作):
 {path: "addAdvertiseSpace",name: "AddAdvertiseSpace",component: () => import("../views/AdvertiseManage/AddAdvertiseSpace"),meta: { requireAuth: true, title: "添加广告位" }},
发现跳转的是AddAdvertiseSpace.vue组件:
js部分(对该组件进行了引用)
<script>
import HomeAdvertiseDetail from "./AdvertiseSpaceDetail";
export default {name: "addHomeAdvertise",title: "添加广告位",components: { HomeAdvertiseDetail }
};
</script>
最后发现是使用了AdvertiseSpaceDetail.vue组件,一般组件里是不能写script标签和style标签的,不会识别
对应js操作
//钩子函数created() {//判断是添加还是修改if(this.isEdit) {//修改}else{//添加this.homeAdvertise = {}//清空数据,防止回显}}, methods: {//方法1: 保存广告位信息handleSave() {this.$refs.form.validate(valid => {if (!valid) return false;//请求后台axios.post("/PromotionSpace/saveOrUpdatePromotionSpace",this.homeAdvertise).then(res => {this.$router.back();//回退到上一个页面}).catch(err => {this.$message.error("保存广告位信息失败");});})},}
修改广告位:
对应的跳转步骤与新增类似,可以自行查看(在UpdateAdvertiseSpace.vue组件)
对应js部分:
  //判断是添加还是修改if(this.isEdit) {//修改const id = this.$route.query.id //使用$route来接收值this.loadPromotionSpace(id)}
 //方法2: 回显广告位信息loadPromotionSpace(id) {return axios.get("/PromotionSpace/findPromotionSpaceById?id=" + id).then(res => {Object.assign(this.homeAdvertise,res.data.content) //当然也可以直接赋值this.homeAdvertise.id =id;}).catch(err => {this.$message.error("回显广告位信息失败");});}
后台访问方法是一样的,就不做说明了
广告管理:
广告列表的展示,使用到了分页组件,接下来通过一个案例演示一下分页插件的使用
ElementUI 分页组件:

https://element.eleme.cn/#/zh-CN/component/pagination
在测试项目中,创建一个PageList.vue组件,复制代码如下(进行了修改)
<template><div><div class="block"><span class="demonstration">完整功能</span><el-pagination@size-change="handleSizeChange"@current-change="handleCurrentChange":current-page="currentPage4":page-sizes="[10, 20, 30, 40]":page-size="40"layout="total, sizes, prev, pager, next, jumper":total="400"></el-pagination><!--
:current-page="currentPage4" 设置一开始的前往第几页,以及当前页,实际上是改变中间显示的位置,即当前页位置
若不在页数范围里,即若超过则默认最大的页数,若低于则默认最小的页数:page-sizes="[100, 200, 300, 400]"设置几页一条的下拉框,也影响中间的总显示,即当前页
:page-size="100"每页显示条数,有时会影响下拉框的操作,即确定下拉框一开始的值
若超过了则会显示超过的,但没有其他修饰:total="400"总条数,会影响中间的总显示,即当前页可以发现:总条数=每页条数+当前页的总显示,所以总条数和每页条数会影响当前页总显示
--></div></div>
</template>
<script>export default {methods: {//显示条数变化时触发handleSizeChange(val) {console.log(`每页 ${val} 条`);},//当前页变化时触发,即中间的总显示变化handleCurrentChange(val) {console.log(`当前页: ${val}`);}},data() {return {currentPage4: 4};}}
</script>
对应路由添加(通过地址将该组件渲染到路由地方):
 {path: "/page",name:"page",component: () => import("@/components/PageList"),}
对应index.vue的添加(这个一般是侧边栏的操作,使得访问地址,进行页面的跳转,将组件放到对应地方显示):
   <el-menu-item-group><!-- 修改 index的路由地址 --><el-menu-item index="/page"><i class="el-icon-menu"></i>分页组件</el-menu-item></el-menu-item-group>
接下来我们可以进行测试了
现在我们搞一个数据,赋值项目的代码到该测试项目里面
修改后的PageList.vue组件:
<template><div class="app-container"><div class="table-container"><el-tableref="homeAdvertiseTable":data="list"style="width: 100%;"v-loading="listLoading"border><el-table-column label="id" width="120" align="center"><template slot-scope="scope">{{scope.row.id}}</template></el-table-column><el-table-column label="广告名称" align="center"><template slot-scope="scope">{{scope.row.name}}</template></el-table-column><el-table-column label="广告图片" width="120" align="center"><template slot-scope="scope"><img style="height: 80px" :src="scope.row.img" /></template></el-table-column></el-table></div><!-- 分页 --><div class="pagination-container"><el-paginationbackground@size-change="handlePageSizeChange"@current-change="handleCurrentPageChange"layout="total, sizes,prev, pager, next,jumper":current-page="page":page-sizes="[5,10, 20]":page-size="size":total="total"></el-pagination></div></div>
</template>
<script>export default {data() {return {list:[], //列表数据  后台传递过来page: 1, //当前页   前端带过去size: 5, //每页显示条数,设置:page-sizes="[5,10, 20]"的初始位置,即这里是5页/条  前端带过去//上面两个是给后台的参数的total: 0, //总条数   后端传递,因为后端不止传递列表数据,还有其分页数据,因为传递的就是分页对象listLoading:true};},//钩子函数created(){this.loadList();},methods: {//获取广告数据loadList(){return //注意:这里之所以要加this,是因为当前组件并没有导入axios,但其他组件已经导入,那么就需要全局//而其他的导入是直接得到对应的axios的,文件夹里封装对应相应名称,就跟变量一样直接使用this.axios.get("http://localhost:8080/ssm_web/PromotionAd/findAllPromotionAdByPage",//这个是后端的访问,前面博客里已经说明了{params:{CurrentPage:this.page,PageSize:this.size}}).then(res => {console.log(res);this.list = res.data.content.list;this.total = res.data.content.total;console.log(this.tolal)this.listLoading=false}).catch(err => {console.log("获取广告数据失败");})},//下面val是修改后的页数//显示条数变化时触发handlePageSizeChange(val) {this.size = val;this.loadList();},//当前页变化时触发,即中间的总显示变化handleCurrentPageChange(val) {this.page = val;this.loadList();}},  //后面加,是没问题的,当然也可以不加,加不加无所谓}
</script>
然后就可以操作对应的分页了
测试完成后,接下来我们来看真正的项目代码
广告列表展示 :
通过观察项目的Advertises.vue组件来操作
我们已经解决了分页问题,接下来再看一下对应组件代码编写:
<template><div class="app-container"><el-card class="operate-container" shadow="never"><el-button size="mini" class="btn-add" @click="handleAdd()">添加广告</el-button></el-card><div class="table-container"><el-tableref="homeAdvertiseTable":data="list"style="width: 100%;"v-loading="listLoading"border><el-table-column label="id" width="120" align="center"><template slot-scope="scope">{{scope.row.id}}</template></el-table-column><el-table-column label="广告名称" align="center"><template slot-scope="scope">{{scope.row.name}}</template></el-table-column><!-- 获取广告位置信息  spaceId 外键id--><el-table-column label="广告位置" width="200" align="center"><template slot-scope="scope">{{getSpaceName(scope.row.spaceId)}}</template></el-table-column><el-table-column label="广告图片" width="120" align="center"><template slot-scope="scope"><img style="height: 80px" :src="scope.row.img" /></template></el-table-column><el-table-column label="时间" width="250" align="center"><template slot-scope="scope"><p>开始时间:{{scope.row.startTime | formatTime}}</p><p>到期时间:{{scope.row.endTime | formatTime}}</p></template></el-table-column><!-- 上线与下线 --><el-table-column label="上线/下线" width="120" align="center"><template slot-scope="scope"><el-switch@change="handleUpdateStatus(scope.row)":active-value="1":inactive-value="0"v-model="scope.row.status"></el-switch></template></el-table-column><!-- 编辑按钮 --><el-table-column label="操作" width="120" align="center"><template slot-scope="scope"><el-button size="mini" type="text" @click="handleUpdate(scope.row)">编辑</el-button></template></el-table-column></el-table></div><!-- 分页 --><div class="pagination-container"><el-paginationbackground@size-change="handlePageSizeChange"@current-change="handleCurrentPageChange"layout="total, sizes,prev, pager, next,jumper":current-page="page":page-sizes="[5,10, 20]":page-size="size":total="total"></el-pagination></div></div>
</template><script>
import { axios } from "../../utils";import moment from "moment";export default {name: "homeAdvertiseList",title: "广告管理",inject: ["reload"],//数据部分data() {return {typeMap: {}, //保存广告位对象信息total: 0, //总条数size: 5, //每页显示条数page: 1, //当前页list: [], //广告数据listLoading: false};},//钩子函数created() {//获取广告列表数据this.loadPromotionAd();//获取广告位数据this.loadPromotionSpace();//打印typeMapconsole.log(this.typeMap); //会先打印,但我们点击时,会出现对应数据,是因为ajax是异步的},methods: {//方法1; 获取广告列表数据loadPromotionAd() {//加载动画是否开启this.listLoading = true;axios.get("/PromotionAd/findAllPromotionAdByPage",{params:{CurrentPage:this.page,PageSize:this.size}}).then(res => {//   res.data.content.list.map(item =>{//     this.typeMap[item.promotionSpace.id] = item.promotionSpace;// }) 这里可以省略掉方法2this.list = res.data.content.list;this.total = res.data.content.total;this.listLoading = false}).catch(err => {this.$message.error("获取广告列表数据失败");})},//方法2: 获取广告位置数据loadPromotionSpace() {//加载动画是否开启,这里加不加都是一样的,因为数据被添加时已经使得不会操作了this.listLoading = true;axios.get("PromotionSpace/findAllPromotionSpace").then(res => {console.log(  res.data.content)//对该content对象使用map方法,将集合的数据依次取出变成itemres.data.content.map(item =>{console.log(item)//将数据保存到typeMap中,其中得到的item的id就是key,所以可以使用item.id,用括号括起来//这里会执行多次,直到没有item的数据//实际上在方法1里就可以操作,因为有这个数据,但这里分开写了,这里就在方法2里写this.typeMap[item.id] = item; //在空的键值对里,我们可以直接指定key,使得创建获得value//而之所以使用id,是因为这个id的广告位的数据的id,那么对应的可以使用外键id来获得})this.listLoading = false;}).catch(err => {this.$message.error("获取广告位置数据失败");})},//方法3: 获取广告位置名称getSpaceName(spaceId) {if(!spaceId){return "";}//返回对应的广告位置名称return this.typeMap[spaceId] && this.typeMap[spaceId].name; //使用外键id来获得名称了//使用&&防止对应数据没有,做个保险},//方法4: 修改状态handleUpdateStatus(row) {},//跳转到新增handleAdd() {this.$router.push({ path: "/addAdvertise" });},//跳转到修改handleUpdate(row) {this.$router.push({ path: "/updateAdvertise", query: { id: row.id } });},//页码变化触发的函数handleCurrentPageChange(page) {this.page = page;this.loadPromotionAd();},//每页显示条数变化触发的函数handlePageSizeChange(size) {this.size = size;this.loadPromotionAd();}},filters: {formatTime(time) {return moment(time).format("YYYY-MM-DD HH:mm:ss");}}
};
</script>
<style scoped>
.input-width {width: 203px;
}
</style>
我们编写了对应js代码,但还有些是我们需要编写的,所以接下来我们来进行操作
广告状态修改:
对应html:
 <!-- 上线与下线 --><el-table-column label="上线/下线" width="120" align="center"><template slot-scope="scope"><el-switch@change="handleUpdateStatus(scope.row)":active-value="1"  :inactive-value="0"v-model="scope.row.status"></el-switch></template></el-table-column><!--active-value: switch:打开时的值 inactive-value : switch 关闭时的值 只要我们点击,他们就会进行切换,然后执行对应方法-->
添加部分对应js部分:
我们首先去https://element.eleme.cn/#/zh-CN/component/message-box(Element-UI)里面找到对应的js代码复制过来并进行操作
//方法4: 修改状态handleUpdateStatus(row) { //row当前这一行的对象
this.$confirm('是否要修改上线/下线状态?', '提示', {confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning'}).then(res => {axios.get("/PromotionAd/updatePromotionAdStatus",{params:{id:row.id,status:row.status}}).then(res => { //当然可以继续使用then,因为方法是返回本身的this.loadPromotionAd()}).catch(err => {this.$message.error("修改状态失败") //不同的操作有不同的显示,可以去Element-UI里找对应的操作//对应地址https://element.eleme.cn/#/zh-CN/component/message})}).catch(err => { //设置取消时出现的操作row.status==1?row.status=0:row.status=1;//之所以这样做,是因为他是先改变才进行方法执行,若不这样操作//那么当你取消时,他却改变了,这是不符合操作的,所以在取消时,需要取反})},
广告新增&修改 :
新增操作(对应的html):
 <el-button size="mini" class="btn-add" @click="handleAdd()">添加广告</el-button>
对应的js:
 //跳转到新增handleAdd() {this.$router.push({ path: "/addAdvertise" });},
对应路由:
  {path: "addAdvertise",name: "AddAdvertise",component: () => import("../views/AdvertiseManage/AddAdvertise"),meta: { requireAuth: true, title: "添加广告" }},
对应的AddAdvertise.vue组件:
<template><home-advertise-detail :isEdit="false"></home-advertise-detail>
</template>
<script>
import HomeAdvertiseDetail from './AdvertiseDetail'
export default {name: 'addHomeAdvertise',title: '添加广告',components: { HomeAdvertiseDetail }
}
</script>
<style></style>
对应的AdvertiseDetail.vue组件(这里进行修改操作,因为最后操作的就是这个组件了):
<template><el-card class="form-container" shadow="never"><!-- 新增&修改广告表单 --><el-form :model="homeAdvertise" :rules="rules" ref="form" label-width="150px" size="small"><el-form-item label="广告名称:" prop="name"><el-input v-model="homeAdvertise.name" class="input-width"></el-input></el-form-item><!-- 广告位下拉列表typeOptions 广告位数据label option的内容value 广告位的id--><el-form-item label="广告位置:"><el-select v-model="homeAdvertise.spaceId"><el-optionv-for="type in typeOptions":key="type.value":label="type.label":value="type.value"></el-option></el-select></el-form-item><el-form-item label="开始时间:" prop="startTime"><el-date-picker type="datetime" placeholder="选择日期" v-model="homeAdvertise.startTime"></el-date-picker></el-form-item><el-form-item label="到期时间:" prop="endTime"><el-date-picker type="datetime" placeholder="选择日期" v-model="homeAdvertise.endTime"></el-date-picker></el-form-item><!-- 上线下线 --><el-form-item label="上线/下线:"><el-radio-group v-model="homeAdvertise.status"><el-radio :label="0">下线</el-radio><el-radio :label="1">上线</el-radio></el-radio-group></el-form-item><!-- 广告图片 --><el-form-item label="广告图片:"><upload-image:content="homeAdvertise.img && [homeAdvertise.img]":get-urls="getADImgUrl"uploadUrl="/PromotionAd/PromotionAdUpload"ref="courseCoverRef"max="10M"tipInfo="建议尺寸:230*300px,JPG、PNG格式,图片小于150K"/></el-form-item><el-form-item label="广告链接:" prop="link"><el-input v-model="homeAdvertise.link" class="input-width"></el-input></el-form-item><el-form-item label="广告备注:"><el-inputclass="input-width"type="textarea":rows="5"placeholder="请输入内容"v-model="homeAdvertise.text"></el-input></el-form-item><el-form-item><el-button type="primary" @click="handleSave()">提交</el-button></el-form-item></el-form></el-card>
</template>
<script>
import UploadImage from "@/components/UploadImage.vue";
import { axios } from "../../utils";//广告对象 defaultHomeAdvertise
const homeAdvertise = {id: undefined,name: null,spaceId: "",img: null,startTime: null,endTime: null,status: 0,link: null,text: null,sort: 0
};
const rules = {name: [{ required: true, message: "请输入广告名称", trigger: "blur" },{min: 2,max: 140,message: "长度在 2 到 140 个字符",trigger: "blur"}],link: [{ required: true, message: "请输入广告链接", trigger: "blur" }],startTime: [{ required: true, message: "请选择开始时间", trigger: "blur" }],endTime: [{ required: true, message: "请选择到期时间", trigger: "blur" }],img: [{ required: true, message: "请选择广告图片", trigger: "blur" }]
};export default {name: "HomeAdvertiseDetail",components: { UploadImage },//组件传参props: {isEdit: {type: Boolean,default: false}},//数据部分data() {return {homeAdvertise, //广告表单对象typeOptions: [], //广告位下拉列表rules};},//钩子函数created() {//判断是新增还是修改if (this.isEdit) {//修改const id = this.$route.query.id;this.loadPromotion(id);} else {//新增this.homeAdvertise = {};}//获取广告位数据this.loadPromotionSpace();},methods: {//方法1: 获取广告位置数据loadPromotionSpace() {return axios.get("/PromotionSpace/findAllPromotionSpace").then(res => {//使用map函数进行遍历,得到所有的广告位信息,实现下拉列表的操作this.typeOptions = res.data.content.map(item => {return {value: item.id,label: item.name};});//前面说过,它会执行很多次,实际上是这个item的箭头函数执行很多次,他也是方法,不要弄错//前面我们只是用来赋值,实际上他1返回值也是一个对象,且是多个return的{}集合//我们可以输出this.typeOptions看一看console.log(this.typeOptions);//发现他输出一个数组,那么就知道,map函数返回一个数组,且对应值是return的值})},//方法2: 保存广告信息handleSave() {this.$refs.form.validate(valid => {if (!valid) return false;if(this.homeAdvertise.id == undefined){if(this.homeAdvertise.startTime != undefined&& this.homeAdvertise.endTime!= undefined){var year = this.homeAdvertise.startTime.getFullYear(); // 年份var month = this.homeAdvertise.startTime.getMonth() + 1; //月份从0开,11结束,所以国内习惯要+1var day = this.homeAdvertise.startTime.getDate(); // 几号var hour = this.homeAdvertise.startTime.getHours(); // 几点var mm = this.homeAdvertise.startTime.getMinutes(); // 分钟var s = this.homeAdvertise.startTime.getSeconds(); //秒this.homeAdvertise.startTime= year+"-"+month+"-"+day+" "+hour+":"+mm+":"+s;year = this.homeAdvertise.endTime.getFullYear(); // 年份month = this.homeAdvertise.endTime.getMonth() + 1; //月份从0开,11结束,所以国内习惯要+1day = this.homeAdvertise.endTime.getDate(); // 几号hour = this.homeAdvertise.endTime.getHours(); // 几点mm = this.homeAdvertise.endTime.getMinutes(); // 分钟s = this.homeAdvertise.endTime.getSeconds(); //秒this.homeAdvertise.endTime= year+"-"+month+"-"+day+" "+hour+":"+mm+":"+s;}}//对应的时间不符合后台格式(因为使用了注解操作json必须为对应的格式),所有这里操作一下对应的格式console.log(this.homeAdvertise.startTime);console.log(this.homeAdvertise.endTime);axios.post("/PromotionAd/saveOrUpdatePromotionAd",this.homeAdvertise).then(res => {//返回上个页面this.$router.back();}).catch(err => {this.$message.error("保存失败");});})},//方法3: 修改回显广告信息loadPromotion(id) {},//获取图片路径,进行回显getADImgUrl(urls) {if (urls.length > 0) {this.homeAdvertise.img = urls && urls[0].filePath;}}}
};
</script>
<style scoped>
.input-width {width: 70%;
}
</style>
修改操作对应的html
 <el-button size="mini" type="text" @click="handleUpdate(scope.row)">编辑</el-button>
对应的js:
 //跳转到修改handleUpdate(row) {this.$router.push({ path: "/updateAdvertise", query: { id: row.id } }); //query传递id参数},
对应的路由:
 {path: "updateAdvertise",name: "UpdateAdvertise",component: () => import("../views/AdvertiseManage/UpdateAdvertise"),meta: { requireAuth: true, title: "编辑广告" }},//可能你会发现,并没有上面操作query,这是因为他只是存放,而并没有使用而已
对应的UpdateAdvertise.vue组件:
<template><home-advertise-detail :isEdit="true"></home-advertise-detail>
</template>
<script>
import HomeAdvertiseDetail from './AdvertiseDetail'
export default {name: 'updateHomeAdvertise',title: '编辑广告',components: { HomeAdvertiseDetail }
}
</script>
<style></style>
发现他最后使用的是AdvertiseDetail组件,与新增是一样的
即我们任然操作AdvertiseDetail组件(实际上修改和添加之所以可以使用一个方法,是因为前端操作了对应标识):
 <home-advertise-detail :isEdit="true"></home-advertise-detail>
<!--修改这就是true,新增就是false,这里简单说明一下-->
然后通过对应的true和false,给对应对象加上id,使得操作新增和修改
接下来我们看一看修改的主要js代码:
  //方法3: 修改回显广告信息loadPromotion(id) {return axios.get("/PromotionAd/findPromotionAdById?id=" + id).then(res => {Object.assign(this.homeAdvertise, res.data.content);//之所以使用上面进行赋值,而不使用下面的,是防止我们已有的信息被覆盖,虽然大多数情况下不会//而使用Object.assign方法,只会赋值我们对应的,若多出一些信息,那么也会添加,即不会消除键key//this.homeAdvertise = res.data.contentthis.homeAdvertise.id = id;}).catch(err => {this.$message.error("回显广告信息失败");})},
用户管理:
分页&条件查询用户数据:
日期选择器组件 :
在查询条件中使用了 ElementUI中的日期选择器,我们一起来简单学习一下日期选择器的使用
https://element.eleme.cn/#/zh-CN/component/date-picker#mo-ren-xian-shi-ri-qi
在测试项目中,创建一个 TestDate.vue组件,复制代码到页面(下面我进行了修改)
注意复制粘贴时,记得组件只能有一个根标签,若报错了基本就是这个错误,在最外面加上div即可(其他也行,但最好div)
对应html:
<template><div class="block"><span class="demonstration">带快捷选项</span><el-date-pickerv-model="dateTime"type="daterange"align="right"unlink-panelsrange-separator="至"start-placeholder="开始日期"end-placeholder="结束日期":picker-options="pickerOptions"></el-date-picker><el-button type="primary" @click="getDate">查询</el-button></div></template><script>export default {data() {return {pickerOptions: {shortcuts: [{text: '最近一周',onClick(picker) {const end = new Date();const start = new Date();start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);picker.$emit('pick', [start, end]);}}, {text: '最近一个月',onClick(picker) {const end = new Date();const start = new Date();start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);picker.$emit('pick', [start, end]);}}, {text: '最近三个月',onClick(picker) {const end = new Date();const start = new Date();start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);picker.$emit('pick', [start, end]);}}]},dateTime: ''};},methods:{getDate() {console.log(this.dateTime); //输出的是数组,即开始时间和结束时间两个组合的数组,且他们都是Date对象//但是发现对应的时分秒都是00:00:00,所以下面的操作是操作对应的时分秒//实际上是操作结束时间的时分秒,,位23:59:59表示这一天的末尾const params = {};params.startCreateTime = this.dateTime[0];//params.startCreateTime改变,那么由于是指向地址,所以也就是this.dateTime[0]改变params.startCreateTime.setHours(0);params.startCreateTime.setMinutes(0);params.startCreateTime.setSeconds(0);params.endCreateTime = this.dateTime[1];params.endCreateTime.setHours(23);params.endCreateTime.setMinutes(59);params.endCreateTime.setSeconds(59);//设置好了对应的时分秒了,否则默认都是0console.log(this.dateTime); console.log(params);//注意:日期的标签,只会识别对应位置,如2022-06-032,最后就是2022-06-03,2不是对应位置即不识别//而超过了对应数,如2022-09-59,会自动进行操作,变成2022-10-29,进行,进日期操作}}};
</script>
对应路由:
  {path: "/date",name:"date",component: () => import("@/components/TestDate"),}
对应侧边栏:
  <el-menu-item-group><!-- 修改 index的路由地址 --><el-menu-item index="/date"><i class="el-icon-menu"></i>日期控件</el-menu-item></el-menu-item-group>
接下来执行项目就可以看到对应效果了
测试完毕后,我们再来操作项目的用户的分页&条件查询
对应的组件是Users.vue组件(发现也是有这个日期的操作的)
首先我们先操作分页&条件查询:
主要的js部分:
 //方法1: 加载用户数据loadUsers() {this.loading = true;//设置日期参数if (this.filter.resTime) {params.startCreateTime = this.filter.resTime[0];params.startCreateTime.setHours(0);params.startCreateTime.setMinutes(0);params.startCreateTime.setSeconds(0);params.endCreateTime = this.filter.resTime[1];params.endCreateTime.setHours(23);params.endCreateTime.setMinutes(59);params.endCreateTime.setSeconds(59);}//设置参数const params = { currentPage: this.page, pageSize: this.size };//请求后台接口return axios.post("/user/findAllUserByPage", params)//之所以这里不用设置年月日的格式,是因为后台是操作json的,json会默认转换Date字符串.then(res => {this.users = res.data.content.list;this.total = res.data.content.total;this.loading = false;}).catch(err => {this.$message.error("获取用户数据失败");});},//注意:这是数据的改变,也就是说数据改变会直接影响页面,而不需要刷新//而像什么添加操作等等,并没有改变本来数据,所以一般需要刷新使得去数据库里再次得到
用户状态设置:
对应的html部分:
 <!-- 禁用按钮  v-if="scope.row.status == 'ENABLE'"--><el-table-column label="操作" align="center" min-width="120"><template slot-scope="scope"><el-buttonsize="mini"type="text"@click="handleToggleStatus(scope.row)">{{ scope.row.status == "ENABLE" ? "禁用" : "启用" }}</el-button><el-buttonsize="mini"type="text"@click="handleSelectRole(scope.$index, scope.row)">分配角色</el-button></template></el-table-column>
对应js部分:
//方法2: 修改用户状态handleToggleStatus(item) {item.status == "ENABLE"?item.status = "DISABLE":item.status = "ENABLE" //取反实现切换//js的三元表达式可以不用获得值,而java必须要return axios.get("/user/updateUserStatus",{params:{id:item.id,status:item.status}}).then(res => {console.log(item.status)console.log(res.data.content)item.status = res.data.content;}).catch(err => {this.$message.error("修改用户状态失败");})},
用户模块的分配角色后面再进行操作
权限管理:
角色管理:
展示&查询角色列表 :
角色组件是 Roles.vue ,在该组件中对角色信息进行管理
一开始得到数据的对应js部分(条件查询也是):
  //获取角色数据loadRoles() {this.listLoading =trueaxios.post("/role/findAllRole",this.listQuery).then(res => {this.list = res.data.content;this.listLoading =false}).catch(err => {this.$message.error("获取角色列表失败");});},
添加&修改角色 :
对应的js:
 //添加&修改角色handleSave() {return axios.post("/role/saveOrUpdateRole",this.role).then(res => {this.loadRoles();this.dialogVisible = false;}).catch(err => {this.$message.error("操作失败");})},
在这里说明一下,在添加数据到sql时,若是字符串的null,即"null"
是可以添加到设置了not null的字段的,因为他与not null的约束的null是不同的,只有单纯的null才不能添加到设置了not null里面
而"null"(字符串的null)可以
而在sql手动加null时,都默认为null,而不是字符串的"null",即出现不了null在字段中,一般只能通过程序来操作
而我们手动添加"null","也是会当作数据的,所以加上null一般需要程序来操作
删除角色的方法:
对应的js:
 //删除角色handleDelete(row) {//在打出confirm时,出现的提示中可以点击帮我们生成下面代码//注意:要是对应的confirm的提示,因为有不同的confirm的提示this.$confirm('是否要删除该角色', '提示', { //这个方法也进行了异步的操作,所以方法外的代码也执行了confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning'}).then(() => {//请求后台axios.get("/role/deleteRole?id=" + row.id).then(res => {this.loadRoles();}).catch(err => {this.$message.error("删除失败");});}).catch(() => {this.$message("已取消");});},
为角色分配菜单:
为角色分配菜单,一个角色可以拥有多个菜单权限
一个菜单权限也可以被多个角色拥有
点击分配菜单,页面展示效果:
前端要实现的效果
第一步:获取到所有的菜单数据,在树形控件中进行展示
第二步:将当前角色拥有的菜单权限,勾选上
菜单展示功能实现:
对应的js:
 //为角色分配菜单handleSelectMenu(row) {this.$router.push({ path: "/allocMenu", query: { roleId: row.id } });},
发现他进行了跳转,对应路由:
       {path: "allocMenu",name: "AllocMenu",component: () =>import(/* webpackChunkName: 'allocMenu' */ "../views/PermissionManage/AllocMenu"),meta: { requireAuth: true, title: "角色菜单管理" }},
找到AllocMenu.vue组件:
对应的html:
<template><el-card class="form-container" shadow="never"><el-tree:data="menuTreeList"show-checkboxdefault-expand-allnode-key="id"ref="tree"highlight-current:props="defaultProps"></el-tree><div style="margin-top: 20px" align="center"><el-button type="primary" @click="handleSave()">保存</el-button><el-button @click="handleClear()">清空</el-button></div></el-card>
</template><script>
import { axios } from "../../utils";export default {name: "allocMenu",title: "角色菜单管理",data() {return {menuTreeList: [], //菜单数据checkedMenuId: [], //被选中的菜单//树形结构子节点设置defaultProps: {children: "subMenuList",label: "name"},roleId: null};},//钩子函数created() {//获取路由携带的idthis.roleId = this.$route.query.roleId;//获取菜单列表this.treeList();//获取角色所拥有的菜单信息this.getRoleMenu(this.roleId);},methods: {//方法1: 获取菜单列表,使用树形控件展示treeList() {},//方法2: 获取当前角色所拥有菜单列表idgetRoleMenu(roleId) {},//方法3: 修改角色所拥有的菜单列表handleSave() {},//清空选择handleClear() {this.$refs.tree.setCheckedKeys([]);}}
};
</script><style scoped>
</style>
操作获取菜单列表数据:
对应的js:
 //方法1: 获取菜单列表,使用树形控件展示treeList() {axios.get("/menu/findAllMenu").then(res => {this.menuTreeList =  res.data.content.parentMenuList //树形控件控的就是数组
//即得到了对应的数组}).catch(err => {this.$message.error("获取菜单列表失败");});},
然后回显对应已有的菜单
对应的js:
//方法2: 获取当前角色所拥有菜单列表idgetRoleMenu(roleId) {axios.get("/menu/findMenuByRoleId?roleId=" + roleId).then(res => {console.log(res.data.content);//将角色已有的菜单权限进行勾选this.$refs.tree.setCheckedKeys(res.data.content);//这是树形控件的操作,指定对应tree,勾选对应的节点//具体可以查询Element-UI的树形控件中的树节点选择的那个地方})},
接下来进行分配:
对应的js部分:
 //方法3: 修改角色所拥有的菜单列表handleSave() {//首先获得他的树形控件数据const ca = this.$refs.tree.getCheckedNodes();  //getCheckedKeys获得选择节点的key来操作,即这里就是id,因为node-key="id"//this.$refs.tree可以看成得到当前文档的tree对应的节点,即树形所有节点console.log(ca)//定义一个获取对应id的变量数组const id = [];//然后循环操作id放入if(ca != null){for(let i =0 ;i<ca.length;i++){const cid = ca[i];//添加对应id放入数组里面id.push(cid.id);//在这个树形控件中,只有当子节点全部选择后,才会选择父节点//但是我们需要局部的操作也要加上父节点,即这里操作加上对应的父节点//而在代码中也是一般先有父节点才会操作子节点的,虽然你可以不用去设置//实际上也不需要去设置,因为只有能显示给你看的,基本就是对应的父子节点关系//但为了顺便加上对应父节点,这里就这样操作了,实际上并不需要,因为添加联系最好还是对应的添加//而不是程序帮忙添加,而为了使得程序顺便添加是要考虑到非常多的情况的,如这里//所以这里需要判断一下,如果当前节点的父节点是否已经在id中,如果不在,则添加进去//使用数组的filter方法来判断,将创建一个数组得到id这个数组,再进行过滤//通过返回的规则进行过滤,满足规则的则保留,不满足的则移除数组中的不满足规则的元素//而由于是创建的,即原数组不会变//解释下面的操作//若数组没有cid.parentId元素,即对应的父元素id,则对应判断相等//那么就说明前面没有父节点,那么就添加进去,当然若当前就是父节点,那么这里也就不会执行了//即下面的操作实现了判断当前是否是父节点,以及数组里面是否有父节点//只要他们有,这个条件就不会为trueif(cid.parentId !== -1 && id.filter(item => item!=cid.parentId).length === id.length){id.push(cid.parentId);}}}//但是上面有个问题//即当加上父节点后,下次回显一般都会默认加上其所有子节点//因为父节点加上了,树形控件会加上其子节点的信息//所以我们在回显的时候,来判断是否添加了全部子节点来进行回显console.log(id)//接下来我们来操作访问后台this.$confirm('是否分配菜单', '提示', {confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning'}).then(() => {const params = {roleId: this.roleId, //角色idmenuIdList: id  //菜单id数组} console.log(params)axios.post("/menu/roleContextMenu",params).then(res => {this.$router.back();}).catch(err => {this.$message.error("分配失败")})}).catch(() => {this.$message("已经取消了")});},
接下来我们来修改一下回显:
 //方法2: 获取当前角色所拥有菜单列表idgetRoleMenu(roleId) {axios.get("/menu/findMenuByRoleId?roleId=" + roleId).then(res => {console.log(res.data.content);//将角色已有的菜单权限进行勾选//先得到返回的id数据const d =[];console.log(this.menuTreeList)console.log(this.menuTreeList.length)for(let a =0;a<this.menuTreeList.length;a++){for(let b =0;b<res.data.content.length;b++){console.log("a:"+this.menuTreeList[a].id)console.log("b:"+res.data.content[b])if(this.menuTreeList[a].id == res.data.content[b]){d.push(res.data.content[b])}}}
console.log(d)//判断对应父节点id是否全部勾选let s =0;for(let c =0;c<d.length;c++){for(let dd =0;dd<this.menuTreeList.length;dd++){if(this.menuTreeList[dd].id==d[c]){for(let j =0;j<this.menuTreeList[dd].subMenuList.length;j++){for(let k =0;k<res.data.content.length;k++){if(this.menuTreeList[dd].subMenuList[j].id==res.data.content[k]){s++;}}}if(s!=this.menuTreeList[dd].subMenuList.length){s=0for(let k =0;k<res.data.content.length;k++){if(res.data.content[k]==this.menuTreeList[dd].id){res.data.content.splice(k,1); //删除数组的k元素,即不是全部勾选的删除}}}}}}console.log("a"+res.data.content);this.$refs.tree.setCheckedKeys(res.data.content);//这是树形控件的操作,指定对应tree,勾选对应的节点//具体可以查询Element-UI的树形控件中的树节点选择的那个地方})},
而由于axios是异步的,即我们需要让回显慢一点运行:
 //钩子函数created() {//获取路由携带的idthis.roleId = this.$route.query.roleId;//获取菜单列表this.treeList();let that = this//获取角色所拥有的菜单信息setTimeout(function(){that.getRoleMenu(that.roleId)},1000);},
但是我们也会发现,若获取菜单列表执行速度超过了1秒钟,那么对应的数据是会出现问题的,所以这里改进一下:
改进后的代码:
 //钩子函数created() {//获取路由携带的idthis.roleId = this.$route.query.roleId;//获取菜单列表this.treeList();let that = this//获取角色所拥有的菜单信息let ki = setInterval(function(){if(that.menuTreeList.length != 0){that.getRoleMenu(that.roleId)clearInterval(ki);}alert(1)},500);},
只有当对应的数组有数据时,也就是得到后台响应的数据时,才会进行方法调用
即一直监听是否得到数据,且间隔0.5秒钟进行一次监听
我们发现,为了实现一个功能,代码一般很多的,而算法就可以解决代码非常多的问题,来进行简化
以后来说明算法的妙用
菜单管理:
对应的组件是Menus.vue组件
首先我们进行展示菜单列表
对应的js:
注意:这里需要在对应后台加上分页操作(因为后台一直是查询所有)
 //方法1: 加载菜单列表数据,且可以进行分页操作loadMenuList() {this.listLoading = true;axios.post("/menu/findAllMenuList",{"currentPage":this.page,"pageSize":this.size}).then(res => {this.list = res.data.content.list;this.total = res.data.content.total;this.listLoading = false;}).catch(err => {this.$message.error("获取菜单列表失败");});},
新增&修改菜单:
新增js:
 //新增菜单跳转handleAddMenu() {this.$router.push("/addMenu");},
对应路由:
 {path: "addMenu",name: "AddMenu",component: () =>import(/* webpackChunkName: 'menuAdd' */ "../views/PermissionManage/AddMenu"),meta: { requireAuth: true, title: "添加菜单" }},
发现是AddMenu.vue组件:
对应的html:
<template><menu-detail :is-edit='false'></menu-detail>
</template>
<script>
import MenuDetail from './MenuDetail'
export default {name: 'addMenu',title: '添加菜单',components: { MenuDetail }
}
</script>
<style>
</style>
发现导入了MenuDetail.vue组件来进行显示:
对应组件的html信息:
<template><el-card class="form-container" shadow="never"><el-form :model="menu" :rules="rules" ref="form" label-width="150px"><el-form-item label="菜单名称:" prop="name"><el-input v-model="menu.name"></el-input></el-form-item><el-form-item label="菜单路径:" prop="href"><el-input v-model="menu.href"></el-input></el-form-item><el-form-item label="上级菜单:"><el-select v-model="menu.parentId" placeholder="请选择菜单"><el-optionv-for="item in selectMenuList":key="item.id":label="item.title":value="item.id"></el-option></el-select></el-form-item><el-form-item label="描述:" prop="description"><el-input v-model="menu.description"></el-input></el-form-item><el-form-item label="前端图标:" prop="icon"><el-input v-model="menu.icon" style="width: 80%"></el-input></el-form-item><el-form-item label="是否显示:"><el-radio-group v-model="menu.shown"><el-radio :label="0">是</el-radio><el-radio :label="1">否</el-radio></el-radio-group></el-form-item><el-form-item label="排序:"><el-input v-model="menu.orderNum"></el-input></el-form-item><el-form-item><el-button type="primary" @click="handleSave()">提交</el-button></el-form-item></el-form></el-card>
</template><script>
import { axios } from "../../utils";const menu = {description: "",parentId: -1,name: "",icon: "",shown: 0,orderNum: 0
};const rules = {description: [{ required: true, message: "请输入菜单描述", trigger: "blur" },{min: 2,max: 140,message: "长度在 2 到 140 个字符",trigger: "blur"}],name: [{ required: true, message: "请输入菜单名称", trigger: "blur" },{min: 2,max: 140,message: "长度在 2 到 140 个字符",trigger: "blur"}],icon: [{ required: true, message: "请输入前端图标", trigger: "blur" },{min: 2,max: 140,message: "长度在 2 到 140 个字符",trigger: "blur"}]
};export default {name: "MenuDetail",props: {isEdit: {type: Boolean,default: false}},data() {return {menu, //菜单对象selectMenuList: [], //下拉列表数据rules};},created() {},methods: {//方法1: 添加或修改 下拉父菜单回显findMenuInfoById(id) {},//保存菜单handleSave() {this.$refs.form.validate(valid => {if (!valid) return false;});}}
};
</script><style scoped>
</style>
首先先操作回显的上级菜单信息:
 created() {if(this.isEdit){//修改
}else{//添加//首先解决回显的初始化this.menu = Object.assign({},this.menu);//获取父菜单信息this.findMenuInfoById(-1)  //-1表示新增,在后台是这样设置的}},
  //方法1: 添加或修改 下拉父菜单回显findMenuInfoById(id) {axios.get("/menu/findMenuInfoById?id=" + id).then(res => {console.log(res.data)//而由于新增和修改都是这个方法,且没有对应的回显方法,即这里需要进行回显,用对应的判断
if(res.data.content.menuInfo != null){//不为null,则是修改操作this.menu = res.data.content.menuInfo;
}//获取下来列表父菜单信息
this.selectMenuList = res.data.content.parentMenuList.map(item => {return {id: item.id,title: item.name};//当然他循环的是对应数组长度//即我们也可以使用这样的次数来操作不是返回值的,也可以操作返回值来实现创建一个新的数组
});
//我们只需要对应的id和name数据,即使用map函数来操作//接下来我们给的显示默认值,而不是-1的默认值
this.selectMenuList.unshift({id:-1,title:"无上级菜单"})//将该对象存放在数组的第一个位置
//而对于Select选择器,由于对于的默认的值设置了-1,那么当有循环这个key得到的id值一样时
//就会使用label的操作,使得变成无上级菜单})},
修改js:
 //修改菜单跳转handleUpdate(index, row) {this.$router.push({ path: "/updateMenu", query: { id: row.id } });},
对应路由:
 {path: "updateMenu",name: "UpdateMenu",component: () =>import(/* webpackChunkName: 'menuUpdate' */ "../views/PermissionManage/UpdateMenu"),meta: { requireAuth: true, title: "编辑菜单" }},
对应的UpdateMenu.vue组件:
<template><menu-detail :is-edit='true'></menu-detail>
</template>
<script>
import MenuDetail from './MenuDetail'
export default {name: 'updateMenu',title: '编辑菜单',components: { MenuDetail }
}
</script>
<style>
</style>
发现最后也是MenuDetail.vue组件
所以现在我们来操作MenuDetail.vue组件的保存操作
最后注意一下:菜单列表是按照主键来排序的,而不是排序字段,因为我们需要有顺序的显示(可能以后需要用到排序字段)
所以可能对应的父菜单是分开的,而像明显显示给我们看的(如菜单列表),一般都会按照主键来排列
新增&修改菜单的保存操作 :
对应的js:
//保存菜单handleSave() {this.$refs.form.validate(valid => {if (!valid) return false;console.log(this.menu)this.menu.createdBy="system"this.menu.updatedBy="system"//上面是为了使得保存时有默认的值,因为mysql的对应字段是设置了not null约束的this.menu.parentId==-1?this.menu.level=0:this.menu.level=1; //判断菜单级别axios.post("/menu/saveOrUpdateMenu",this.menu).then(res => {this.$router.back();}).catch(err => {this.$message.error("操作失败");});});}
当我们编辑时,也有进行回显,改变钩子函数:
  created() {if(this.isEdit){//修改 回显菜单信息
const id = this.$route.query.id//获取父菜单信息this.findMenuInfoById(id) //回显对应的参数指定的id对应的菜单信息
}else{//添加//首先解决回显的初始化this.menu = Object.assign({},this.menu);//获取父菜单信息this.findMenuInfoById(-1)  //-1表示新增,在后台是这样设置的}},
那么我们就可以测试了
注意:我们在后台的菜单级数并没有操作,也就是说默认为0,即一级,所以这里需要你去判断设置一下
最后:由于js可以直接指定元素赋值,如一个空的var a ={},你直接指定的a.id=1,那么对应的就会有a这个参数创建
所以vue的双向绑定时,实际上对应的没有也会创建的,但若对应的直接获取,自然是找不到的,即报错
删除功能:
回到原来的Menus.vue组件:
对应删除的js:
  //删除功能handleDelete(index, row) {this.$confirm('是否删除该菜单信息', '提示', {confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning'}).then(() => {axios.get("/menu/deleteMenu?id=" + row.id).then(res => {this.loadMenuList();}).catch(err => {this.$message.error("删除失败");})}).catch(() => {this.$message('已取消删除');});}
资源管理:
对应的组件是Resources.vue组件:
获取资源信息的js:
 //方法1: 获取资源数据getResourceList() {this.listLoading = trueaxios.post("/resource/findAllResourceByPage",this.listQuery) //this.listQuery的对应信息我们输入时就会提交过去,这就是双向绑定的好处.then(res => {this.list = res.data.content.list;this.total = res.data.content.total;this.listLoading = false}).catch(err => {this.$message.error("获取资源数据失败");});},
注意:有些地方不起作用,最好自己去解决(提示:重置)
获取分类信息的js:
 //方法2: 获取资源分类数据getResourceCateList() {axios.get("/resource/findAllResourceCategory").then(res => {this.cateList = res.data.content //这个可以不写,但我们可以用他来保存对应的数据,在以后使用//将该数据保存到下拉列表中,保存对应的名称,但这里需要指定对应名称for(let k =0;k<this.cateList.length;k++){const ca = this.cateList[k]this.categoryOptions.push({label:ca.name,value:ca.id}) //设置下拉列表,和对应的id传递}}).catch(err => {this.$message.error("获取资源分类数据失败");})},
新增&修改资源:
对应的新增js:
首先是对弹出来的框框进行操作显示
 //添加资源回显,开启对应的框框handleAdd() {this.isEdit = falsethis.dialogVisible = true  //顺序可以很重要的,虽然速度太快,基本无影响this.resource = Object.assign({},defaultResource) //初始化对应的回显数据,后面的就是用来初始化的},
主要添加资源操作js:
注意:对应后台并没有对应的资源的添加,修改,删除,所以需要自己去编写
 //添加&修改资源handleSave() {console.log(this.resource)//由于this.resource里面的值是默认存在的,即并没有通过绑定来创建参数//即后台的设置"null"是会被覆盖的,所以这里进行操作if(this.resource.name == null) this.resource.name = "null"if(this.resource.url == null)this.resource.url = "null"axios.post("/resource/saveOrUpdateResource",this.resource).then(res => { //注意:一般设置了json,传递时就要是json的格式,否则是会报错的this.getResourceList();this.dialogVisible = false;}).catch(err => {this.$message.error("添加或修改资源失败");})},//注意:一般前端没有设置表单约束,而这个没有设置的,一般可能会与后台冲突,即冲突那么就会报错//为了防止这种情况,我们一般操作报错信息,当然我们也会设置特殊情况,如上面的"null"字符串//但是这里要注意,最好不要设置"null"字符串,这是为了方便数据库的数据合理性//但这里设置了,这是为了降低错误而已,实际情况下基本都会有表单约束的
修改操作也是需要回显框框:
对应的js如下:
 //编辑资源 回显handleUpdate(row) {this.isEdit = truethis.dialogVisible = true  this.resource = Object.assign({},row) //将row拷贝到{}里面,相当于就是得到row//但他们虽然值相同,但地址不同,所以==连接时是falseconsole.log(row)console.log(this.resource)},
保存的操作与添加操作的保存是一样的
后面的资源分类以及对应的删除资源通过上面的操作,可以自己进行编写了
还有对应的分配资源
注意:我们进行分页时使用的Mybatis的分页操作的总条数是查询的条数,然后根据对应的分页操作来获取
即当我们没有数据时,对应的总条数一般都为0,而前端是虽然总条数可以设置为0或者负数
但对应的当前页,在经过总条数时,就算再小也不会超过1,即一般最新的当前页就是第一页
正是因为这样,所以我们可以操作删除进行当前页的减小,从而实现当删除当前页的最后一个数据时,进行到前一页
对应的提示:一般设置对应的当前页–操作,来实现到前一页
用户权限控制:
对应的组件是:Login.vue组件
对应的js部分
 //发送登录请求this.$store.dispatch('createToken', this.model) //执行了createToken方法.then(res => {//...后面省略
我们去找到createToken方法,在store的actions.js里面:
对应的js:
 //请求后台登录接口,被注释了,我们可以解开,这样就使得不会随便的输入就能登录了// const res = await TokenService.userLogin({//   phone: username.trim(),//   password: password.trim()// });//前面有import { TokenService, UserService } from "../services";
总体js操作:
/*** 创建新的客户端令牌,可以这样说*/createToken: async ({ commit }, { username, password }) => {//请求后台登录接口const res = await TokenService.userLogin({phone: username.trim(),password: password.trim()});//const res = login();console.log(res);//判断结果不等于1,登录失败if (res.state !== 1) {return Promise.resolve(res);}//获取到content//const result = JSON.parse(res.content);const result = res.content;//将token保存在 sessioncommit(CHANGE_SESSION, {accessToken: result.access_token//refreshToken: result.refresh_token});return res;},
找到services里面的js(好像是tokens.js),即可以看看对应的userLogin方法:
//登录请求
export const userLogin = async (data) => {return await PostRequest(`${process.env.VUE_APP_API_FAKE}/user/login?${Serialize(data)}`)
}
Login.vue组件不省略的js:
//提交登录表单submit (ref) {// form validatethis.$refs[ref].validate(valid => {if (!valid) return false// validatedthis.error = nullthis.loading = true// create token from remote//发送登录请求this.$store.dispatch('createToken', this.model) //执行了createToken方法.then(res => {if (res.state !== 1) {  //但是返回1时,说明登录成功this.error = {title: 'Error occurred',message: 'Abnormal, please try again later!'}switch (res.state) {case 201:this.error.message = '请输入正确的手机号'breakcase 202:this.error.message = '请输入密码'breakcase 203:this.error.message = '密码错误'breakcase 204:this.error.message = '验证码过期'breakcase 205:this.error.message = '帐号错误或密码错误'breakcase 206:this.error.message = '帐号错误或密码错误'breakcase 207:this.error.message = '验证码错误'breakcase 500:this.error.message ='Internal server error, please try again later!'break}this.loading = falsereturn}this.loading = false //这个与按钮有关,也可以不写//登录成功就会到这里来,对应的'/'也就是首页了this.$router.replace({ path: this.$route.query.redirect || '/' })}).catch(err => {console.error(err)this.error = {title: 'Error occurred',message: 'Abnormal, please try again later!'}switch (err.response && err.response.status) {case 401:this.error.message = 'Incorrect username or password!'breakcase 500:this.error.message ='Internal server error, please try again later!'break}this.loading = false})})}
动态获取用户菜单:
而我们使用vue时,之所以基本只会显示对应的组件,是因为对应的js并没有运行造浏览器
他们通过js操作返回给我们一个网页,即会发现基本其他的组件也看不到
在我们登录成功后,会立即发送第二个请求,来获取用户的菜单权限列表
对应的在store的actions.js里面:
/*** 获取当前登录用户权限*/getUserPermissions: async ({ commit }) => {//console.log("访问任何页面都会经过我");//const res = fetchUserPermissions();const res = UserService.getUserPermissions();//...后面省略
对应的方法在users.js里面,对应的getUserPermissions方法:
export const getUserPermissions = async () => {return await baseRequest(`${bossBase}/permission/getUserPermissions`)
}
发现也是传递请求
注意:当跨域请求时,对应的cookie不会传递过去,即不会携带,也就是说,这样的使用cookie来操作的可能会操作不了
但我们的前端项目已经操作可以的,所以不用去解决,具体位置在如下:
在对应的utils里面的axios.js可以发现(我们导入的也是这个,他进行了再次导入):
axios.defaults.withCredentials = true;
//默认为false,跨域不会携带cookie的
//所以在以前的博客里说过,跨域其实也是设置的(好像是58章博客里面的跨域问题),所以服务器只是解决跨域访问
//当我们设置后,对应的操作可以传递cookie,当服务器看到有cookie自然就会给出cookie,否则是不会给出的
//所以对应的数据提交以及接收就需要对应的前端来设置
//有一个这样的代码,就表示跨域时,携带cookie
且要注意:若出现了不对劲的地方,可以重启项目,使得刷新
因为当项目够大时,对应的部署可能会有缺陷,但通常不会
一般的,当后台相应给我们对应的token时,我们每次的请求都基本是需要访问一次的
但这样的每次访问是进行操作的,看如下:
导航守卫:
在执行路由之前先执行的一些钩子函数,比如验证用户是否有权限之类的操作,就需要使用
authorize.js 中配置了导航守卫,来对用户的登录进行限制
在plugins里面找到authorize.js:
对应的js
/*** Check login state* Some middleware to help us ensure the user is authenticated.*/import router from "../router";
import store from "../store";export default Vue => {//导航守卫 to要访问的url, from从哪个路径跳转过来, next() 放行// Authorize (Make sure that is the first hook.)router.beforeHooks.unshift((to, from, next) => {// don't need authorizeif (!to.meta.requireAuth) return next();// check login statestore.dispatch("checkToken").then(valid => {// authorizedif (valid) {store.dispatch('getUserPermissions').then(res => {const { memusMap } = resif (memusMap.Courses && to.name === 'Home') {return next()} else if (memusMap[to.name]) {return next()} else if (Object.keys(memusMap).length > 0) {return next({ name: memusMap[Object.keys(memusMap)[0]].name })} else {next({ name: 'PermissionDenied' })}})return next();}// unauthorizedconsole.log("Unauthorized");next({ name: "Login", query: { redirect: to.fullPath } });});});// login page visiablerouter.beforeEach((to, from, next) => {if (to.name !== "Login") return next();// check login statestore.dispatch("checkToken").then(valid => {if (!valid) return next();// when logged inconsole.log("Authorized");next({ path: to.query.redirect || "/" });});});
};
这些只是对应的基础格式,了解即可
分配角色:
我们再次回到用户模块的Users.vue组件,现在我们来操作分配角色:
对应的html:
<el-button size="mini" type="text" @click="handleSelectRole(scope.$index, scope.row)">分配角色</el-button
对应的handleSelectRole方法(回显窗口):
//分配角色handleSelectRole(index, row) {this.allocAdminId = row.id;this.getRoleList();  //获取角色列表this.getUserRoleById(row.id);  //回显对应角色this.allocDialogVisible = true;},
获取角色列表:
 //获取角色列表getRoleList(){axios.post("/role/findAllRole",{}).then(res => {this.allRoleList = res.data.content;
console.log(this.allRoleList)}).catch(err => {this.$message.error("获取角色列表失败");})},
获取用户的角色:
//回显对应角色getUserRoleById(id){axios.get("/user/findUserRelationRoleById?id=" + id).then(res => {const all = res.data.content;console.log(all)this.allocRoleIds =[];if(all != null && all.length >0){for(let i=0;i<all.length;i++){this.allocRoleIds.push(all[i].id); //在控件里加了multiple,使得操作数组数据,即默认多条数据}}console.log(this.allocRoleIds)console.log(this.allRoleList)}).catch(err => {this.$message.error("回显对应角色失败");})},
最后分配角色:
注意:虽然js的key可以不写分号,但对与json来说是要加上的
而一些插件也会默认给对应的key加上分号,所以axios对应的参数的key可以不加,因为最后帮忙加上了
当然了若有分号自然不会加的,这是判断操作,分号是包括’'(单引号)和""(双引号)
在没有特殊的操作下,他们的最终结果是一样的
若有特殊情况:如java的’'(单引号)和""(双引号),他们就是不同的,但对于js来说他们可以看成是一个结果
但也要注意:字符串只是对数标识,实际上他只是给我们显示看的,最后都是二进制,但正是由于这个标识使得二进制不同
但可以显示相同,如java里面字符串"1"和整数1的显示一般是相同的,乱码除外
 //确认分配角色handleAllocDialogConfirm() {const params = {userId:this.allocAdminId, roleIdList: this.allocRoleIds};axios.post("/user/userContextRole", params).then(res => {this.loadUsers();this.allocDialogVisible = false;}).catch(err => {this.$message.error("分配角色失败");});},

74-SSM项目实战前端开发相关推荐

  1. 视频教程-20年Nodejs教程零基础入门到项目实战前端视频教程-Node.js

    20年Nodejs教程零基础入门到项目实战前端视频教程 7年的开发架构经验,曾就职于国内一线互联网公司,开发工程师,现在是某创业公司技术负责人, 擅长语言有node/java/python,专注于服务 ...

  2. Python项目实战:开发PetStore宠物商店项目-关东升-专题视频课程

    Python项目实战:开发PetStore宠物商店项目-487人已学习 课程介绍         课程内容包括项目分析与设计过程.数据库设计过程.项目敏捷开发.MySQL数据库.Python访问数据库 ...

  3. SSM项目实战:酒店管理系统

    使用的技术栈:Spring+SpringMVC+mybatis+Mysql+layui+Maven Maven 项目结构.项目配置项为: 服务器:apache-tomcat-9.0.0.M26 (必须 ...

  4. web前端开发做项目,前端开发学习教程

    毕业工作一年之后,有了转行的想法,偶然接触到程序员这方面,产生了浓厚且强烈的兴趣,开始学习前端,成功收割了大厂offer,开始了我的程序员生涯. 在自学过程中有过一些小厂的面试经历,也在一些小型的互联 ...

  5. .net core项目实战之开发环境搭建

    在上一篇[.net core项目实战之回顾总结]主要介绍了项目背景和自己的一些想法,从本篇开始正式叙述整个开发过程,本篇主要介绍一下开发前的环境准备,vs2017和docker的安装与配置 系统要求 ...

  6. Vue应用框架整合与实战--前端开发生态圈

    向着阳光,我们意念坚定不移,相信未来属于我们! Javascript Article Article Javascript深浅拷贝 Javascript中的apply和call继承 Javascrip ...

  7. 『收藏向 期末SSM课设救急』 教你从搭建到测试运行手撸一个SSM项目实战,附带源码,前端页面、解析和一般遇到的问题(排雷)

  8. java调查问卷系统-投票系统-SSM项目实战

    课程基于Layui+Spring+SpringMVC+MyBatis开发小而完整的调查问卷系统实战 源码地址 链接:https://pan.baidu.com/s/1PCr0FUYevO9VsKIwd ...

  9. 微信小程序完整项目实战(前端+后端)

    基于微信小程序的在线商城点单系统 前言:闲来无事,想以后自己开一个小超市或者小吃店,能够支持线上下单,既方便客户也方便自己.系统采用C#语言作为后端实现与小程序的交互,给用来学习或者想自己开个小店的朋 ...

  10. SSM项目实战(SSM商城 Maven项目 商品、评论、回复)

    目录 前言 主要功能 开发环境 数据库设计 项目架构 项目技术介绍 运行效果展示 配置文件 项目源代码 总结 前言 SSM框架(spring+springMVC+mybatis),是目前比较主流的Ja ...

最新文章

  1. oracle 截取中英文混合_C语言截取中英文混合字符串
  2. QT. 学习之路 一
  3. hdu-5068 Harry And Math Teacher
  4. Parity Alternated Deletions
  5. boost::log模块测量转储二进制数据的性能
  6. TCP/UDP 套接字总结
  7. http并发,操作系统如何识别对应的进程,线程请求
  8. linux如何设置mac快捷键,在Ubuntu上使用macOS的快捷键
  9. mysql proxy 多主_mysql多主多从架构与mysql-proxy读写分离
  10. 扎心!全国6.5亿网民月收入不足5000元
  11. 疯狂python讲义pdf_如何自学成Python大神?这份学习宝典火爆 IT 圈!
  12. 计算机 仿真 流体力学剪切应力,基于人体血管B型主动脉夹层三维建模及血流动力学仿真研究...
  13. 软件测试(三)--标准的测试用例模板
  14. 如何写项目文档?项目文档有哪些?
  15. SolidWorks2010
  16. 湖南科技大学EDA作业
  17. 阿里云 CentOS7.9 搭建 Hexo 个人博客教程
  18. ios正式包ipa,发布苹果应用商店App Store
  19. 怎么用计算机搜索文件夹,如何在电脑中查找文件_如何在电脑里查找文件-win7之家...
  20. gSOAP 源码分析(四)

热门文章

  1. SSD固态硬盘一键分区后如何检测4K对齐?
  2. C语言:查找数组中最小的元素
  3. 最好的注册测绘师考试资料大全
  4. linux 系统中编译exe文件,在linux系统下执行C#编译的exe文件
  5. Cmder的安装与配置
  6. 大学生心理健康管理系统
  7. 网络编程项目——在线电子词典
  8. powerbuilder11.5 免安装 时的注意事项
  9. 吉林大学计算机游戏程序设计,吉林大学在2018年大学生程序设计竞赛中夺得佳绩...
  10. 趋势科技防毒墙-网络版(OfficeScan)客户端管理工具