谷粒学苑项目前台界面 (一)
框架目录结构:在 plugins 目录下创建 nuxt-swiper-plugin.js,配置插件在 nuxt.config.js 文件中配置插件将 plugins 和 css节点 复制到 module.exports节点下index.vue 页面静态轮播图实现了自动播放路径是固定地址,不发生变化使用标签实现跳转, to: 跳转的地址在 pages 中创建 course 文件夹,文件夹下创建 in
谷粒学苑项目前台界面
项目分为三篇:
谷粒学苑项目前置知识
谷粒学苑项目前台界面: 由于字数限制,分为俩部分,此篇为第一部分,第二部分
谷粒学苑后台管理系统
额外增加的功能:
后台 课程 小节的 删改 操作 🆗
课程列表的 分页查询和 条件查询 🆗
前台 banner 图的自动播放 🆗
后台 banner 的增删改 🆗
后台 对 前台轮播图的图片数量做一个设置。比如设置 5 张图片轮播,设置 3张图片轮播 🆗
课程详情
全部
按钮的实现 🆗课程评论功能🆗
资料链接:谷粒学苑
提取码:p6er
视频教程:尚硅谷-谷粒学苑
一、使用 Nuxt 前台环境搭建
将下载好的模板,里面的 template 放到 VSCode 中的工作区。
在集成终端中打开该项目,使用
npm install
安装依赖启动:
npm run dev
启动之后有一些警告,是不影响运行的,只要不是 error 就行
框架目录结构:
(1)资源目录 assets
用于组织未编译的静态资源如 LESS、SASS 或 JavaScript。
(2)组件目录 components
用于组织应用的 Vue.js 组件。Nuxt.js 不会扩展增强该目录下 Vue.js 组件,即这些组件不会像页面组件那样有 asyncData 方法的特性。
(3)布局目录 layouts
设置页面的布局方式
(4)页面目录 pages
存放页面, .vue 页面
(5)插件目录 plugins
用于组织那些需要在 根vue.js应用 实例化之前需要运行的 Javascript 插件。
(6)nuxt.config.js 文件
nuxt.config.js 文件用于组织Nuxt.js 应用的个性化配置,以便覆盖默认配置。
在 default.vue 中只定义页面的头部,和尾部,中间引入其他组件
二、前台首页静态显示
- 下载幻灯片插件
npm install vue-awesome-swiper@3.1.3
- 在 plugins 目录下创建 nuxt-swiper-plugin.js,配置插件
import Vue from 'vue'
import VueAwesomeSwiper from 'vue-awesome-swiper/dist/ssr'
Vue.use(VueAwesomeSwiper)
-
在 nuxt.config.js 文件中配置插件
将 plugins 和 css节点 复制到 module.exports节点下
module.exports = {
// some nuxt config...
plugins: [
{ src: '~/plugins/nuxt-swiper-plugin.js', ssr: false }
],
css: [
'swiper/dist/css/swiper.css'
]
}
- 赋复静态资源到 项目assets 目录下
- default 页面布局
<template>
<div class="in-wrap">
<!-- 公共头引入 -->
<header id="header">
<section class="container">
<h1 id="logo">
<a href="#" title="谷粒学院">
<img src="~/assets/img/logo.png" width="100%" alt="谷粒学院">
</a>
</h1>
<div class="h-r-nsl">
<ul class="nav">
<router-link to="/" tag="li" active-class="current" exact>
<a>首页</a>
</router-link>
<router-link to="/course" tag="li" active-class="current">
<a>课程</a>
</router-link>
<router-link to="/teacher" tag="li" active-class="current">
<a>名师</a>
</router-link>
<router-link to="/article" tag="li" active-class="current">
<a>文章</a>
</router-link>
<router-link to="/qa" tag="li" active-class="current">
<a>问答</a>
</router-link>
</ul>
<!-- / nav -->
<ul class="h-r-login">
<li id="no-login">
<a href="/sing_in" title="登录">
<em class="icon18 login-icon"> </em>
<span class="vam ml5">登录</span>
</a>
|
<a href="/sign_up" title="注册">
<span class="vam ml5">注册</span>
</a>
</li>
<li class="mr10 undis" id="is-login-one">
<a href="#" title="消息" id="headerMsgCountId">
<em class="icon18 news-icon"> </em>
</a>
<q class="red-point" style="display: none"> </q>
</li>
<li class="h-r-user undis" id="is-login-two">
<a href="#" title>
<img
src="~/assets/img/avatar-boy.gif"
width="30"
height="30"
class="vam picImg"
alt
>
<span class="vam disIb" id="userName"></span>
</a>
<a href="javascript:void(0)" title="退出" onclick="exit();" class="ml5">退出</a>
</li>
<!-- /未登录显示第1 li;登录后显示第2,3 li -->
</ul>
<aside class="h-r-search">
<form action="#" method="post">
<label class="h-r-s-box">
<input type="text" placeholder="输入你想学的课程" name="queryCourse.courseName" value>
<button type="submit" class="s-btn">
<em class="icon18"> </em>
</button>
</label>
</form>
</aside>
</div>
<aside class="mw-nav-btn">
<div class="mw-nav-icon"></div>
</aside>
<div class="clear"></div>
</section>
</header>
<!-- /公共头引入 -->
<nuxt/>
<!-- 公共底引入 -->
<footer id="footer">
<section class="container">
<div class>
<h4 class="hLh30">
<span class="fsize18 f-fM c-999">友情链接</span>
</h4>
<ul class="of flink-list">
<li>
<a href="http://www.atguigu.com/" title="尚硅谷" target="_blank">尚硅谷</a>
</li>
</ul>
<div class="clear"></div>
</div>
<div class="b-foot">
<section class="fl col-7">
<section class="mr20">
<section class="b-f-link">
<a href="#" title="关于我们" target="_blank">关于我们</a>|
<a href="#" title="联系我们" target="_blank">联系我们</a>|
<a href="#" title="帮助中心" target="_blank">帮助中心</a>|
<a href="#" title="资源下载" target="_blank">资源下载</a>|
<span>服务热线:010-56253825(北京) 0755-85293825(深圳)</span>
<span>Email:info@atguigu.com</span>
</section>
<section class="b-f-link mt10">
<span>©2018课程版权均归谷粒学院所有 京ICP备17055252号</span>
</section>
</section>
</section>
<aside class="fl col-3 tac mt15">
<section class="gf-tx">
<span>
<img src="~/assets/img/wx-icon.png" alt>
</span>
</section>
<section class="gf-tx">
<span>
<img src="~/assets/img/wb-icon.png" alt>
</span>
</section>
</aside>
<div class="clear"></div>
</div>
</section>
</footer>
<!-- /公共底引入 -->
</div>
</template>
<script>
import "~/assets/css/reset.css";
import "~/assets/css/theme.css";
import "~/assets/css/global.css";
import "~/assets/css/web.css";
export default {};
</script>
- index.vue 页面静态
- 轮播图实现了自动播放
<template>
<div>
<!-- 幻灯片 开始 -->
<!-- 幻灯片 开始 -->
<div v-swiper:mySwiper="swiperOption" >
<div class="swiper-wrapper">
<div class="swiper-slide" style="background: #040b1b">
<a target="_blank" href="/">
<img
src="~/assets/photo/banner/1525939573202.jpg"
alt="首页banner"
/>
</a>
</div>
<div class="swiper-slide" style="background: #040b1b">
<a target="_blank" href="/">
<img
src="~/assets/photo/banner/153525d0ef15459596.jpg"
alt="首页banner"
/>
</a>
</div>
</div>
<div class="swiper-pagination swiper-pagination-white"></div>
<div
class="swiper-button-prev swiper-button-white"
slot="button-prev"
></div>
<div
class="swiper-button-next swiper-button-white"
slot="button-next"
></div>
</div>
<!-- 幻灯片 结束 -->
<!-- 幻灯片 结束 -->
<div id="aCoursesList">
<!-- 网校课程 开始 -->
<div>
<section class="container">
<header class="comm-title">
<h2 class="tac">
<span class="c-333">热门课程</span>
</h2>
</header>
<div>
<article class="comm-course-list">
<ul class="of" id="bna">
<li>
<div class="cc-l-wrap">
<section class="course-img">
<img
src="~/assets/photo/course/1442295592705.jpg"
class="img-responsive"
alt="听力口语"
/>
<div class="cc-mask">
<a href="#" title="开始学习" class="comm-btn c-btn-1"
>开始学习</a
>
</div>
</section>
<h3 class="hLh30 txtOf mt10">
<a
href="#"
title="听力口语"
class="course-title fsize18 c-333"
>听力口语</a
>
</h3>
<section class="mt10 hLh20 of">
<span class="fr jgTag bg-green">
<i class="c-fff fsize12 f-fA">免费</i>
</span>
<span class="fl jgAttr c-ccc f-fA">
<i class="c-999 f-fA">9634人学习</i>
|
<i class="c-999 f-fA">9634评论</i>
</span>
</section>
</div>
</li>
<li>
<div class="cc-l-wrap">
<section class="course-img">
<img
src="~/assets/photo/course/1442295581911.jpg"
class="img-responsive"
alt="Java精品课程"
/>
<div class="cc-mask">
<a href="#" title="开始学习" class="comm-btn c-btn-1"
>开始学习</a
>
</div>
</section>
<h3 class="hLh30 txtOf mt10">
<a
href="#"
title="Java精品课程"
class="course-title fsize18 c-333"
>Java精品课程</a
>
</h3>
<section class="mt10 hLh20 of">
<span class="fr jgTag bg-green">
<i class="c-fff fsize12 f-fA">免费</i>
</span>
<span class="fl jgAttr c-ccc f-fA">
<i class="c-999 f-fA">501人学习</i>
|
<i class="c-999 f-fA">501评论</i>
</span>
</section>
</div>
</li>
<li>
<div class="cc-l-wrap">
<section class="course-img">
<img
src="~/assets/photo/course/1442295604295.jpg"
class="img-responsive"
alt="C4D零基础"
/>
<div class="cc-mask">
<a href="#" title="开始学习" class="comm-btn c-btn-1"
>开始学习</a
>
</div>
</section>
<h3 class="hLh30 txtOf mt10">
<a
href="#"
title="C4D零基础"
class="course-title fsize18 c-333"
>C4D零基础</a
>
</h3>
<section class="mt10 hLh20 of">
<span class="fr jgTag bg-green">
<i class="c-fff fsize12 f-fA">免费</i>
</span>
<span class="fl jgAttr c-ccc f-fA">
<i class="c-999 f-fA">300人学习</i>
|
<i class="c-999 f-fA">300评论</i>
</span>
</section>
</div>
</li>
<li>
<div class="cc-l-wrap">
<section class="course-img">
<img
src="~/assets/photo/course/1442302831779.jpg"
class="img-responsive"
alt="数学给宝宝带来的兴趣"
/>
<div class="cc-mask">
<a href="#" title="开始学习" class="comm-btn c-btn-1"
>开始学习</a
>
</div>
</section>
<h3 class="hLh30 txtOf mt10">
<a
href="#"
title="数学给宝宝带来的兴趣"
class="course-title fsize18 c-333"
>数学给宝宝带来的兴趣</a
>
</h3>
<section class="mt10 hLh20 of">
<span class="fr jgTag bg-green">
<i class="c-fff fsize12 f-fA">免费</i>
</span>
<span class="fl jgAttr c-ccc f-fA">
<i class="c-999 f-fA">256人学习</i>
|
<i class="c-999 f-fA">256评论</i>
</span>
</section>
</div>
</li>
<li>
<div class="cc-l-wrap">
<section class="course-img">
<img
src="~/assets/photo/course/1442295455437.jpg"
class="img-responsive"
alt="零基础入门学习Python课程学习"
/>
<div class="cc-mask">
<a href="#" title="开始学习" class="comm-btn c-btn-1"
>开始学习</a
>
</div>
</section>
<h3 class="hLh30 txtOf mt10">
<a
href="#"
title="零基础入门学习Python课程学习"
class="course-title fsize18 c-333"
>零基础入门学习Python课程学习</a
>
</h3>
<section class="mt10 hLh20 of">
<span class="fr jgTag bg-green">
<i class="c-fff fsize12 f-fA">免费</i>
</span>
<span class="fl jgAttr c-ccc f-fA">
<i class="c-999 f-fA">137人学习</i>
|
<i class="c-999 f-fA">137评论</i>
</span>
</section>
</div>
</li>
<li>
<div class="cc-l-wrap">
<section class="course-img">
<img
src="~/assets/photo/course/1442295570359.jpg"
class="img-responsive"
alt="MySql从入门到精通"
/>
<div class="cc-mask">
<a href="#" title="开始学习" class="comm-btn c-btn-1"
>开始学习</a
>
</div>
</section>
<h3 class="hLh30 txtOf mt10">
<a
href="#"
title="MySql从入门到精通"
class="course-title fsize18 c-333"
>MySql从入门到精通</a
>
</h3>
<section class="mt10 hLh20 of">
<span class="fr jgTag bg-green">
<i class="c-fff fsize12 f-fA">免费</i>
</span>
<span class="fl jgAttr c-ccc f-fA">
<i class="c-999 f-fA">125人学习</i>
|
<i class="c-999 f-fA">125评论</i>
</span>
</section>
</div>
</li>
<li>
<div class="cc-l-wrap">
<section class="course-img">
<img
src="~/assets/photo/course/1442302852837.jpg"
class="img-responsive"
alt="搜索引擎优化技术"
/>
<div class="cc-mask">
<a href="#" title="开始学习" class="comm-btn c-btn-1"
>开始学习</a
>
</div>
</section>
<h3 class="hLh30 txtOf mt10">
<a
href="#"
title="搜索引擎优化技术"
class="course-title fsize18 c-333"
>搜索引擎优化技术</a
>
</h3>
<section class="mt10 hLh20 of">
<span class="fr jgTag bg-green">
<i class="c-fff fsize12 f-fA">免费</i>
</span>
<span class="fl jgAttr c-ccc f-fA">
<i class="c-999 f-fA">123人学习</i>
|
<i class="c-999 f-fA">123评论</i>
</span>
</section>
</div>
</li>
<li>
<div class="cc-l-wrap">
<section class="course-img">
<img
src="~/assets/photo/course/1442295379715.jpg"
class="img-responsive"
alt="20世纪西方音乐"
/>
<div class="cc-mask">
<a href="#" title="开始学习" class="comm-btn c-btn-1"
>开始学习</a
>
</div>
</section>
<h3 class="hLh30 txtOf mt10">
<a
href="#"
title="20世纪西方音乐"
class="course-title fsize18 c-333"
>20世纪西方音乐</a
>
</h3>
<section class="mt10 hLh20 of">
<span class="fr jgTag bg-green">
<i class="c-fff fsize12 f-fA">免费</i>
</span>
<span class="fl jgAttr c-ccc f-fA">
<i class="c-999 f-fA">34人学习</i>
|
<i class="c-999 f-fA">34评论</i>
</span>
</section>
</div>
</li>
</ul>
<div class="clear"></div>
</article>
<section class="tac pt20">
<a href="#" title="全部课程" class="comm-btn c-btn-2">全部课程</a>
</section>
</div>
</section>
</div>
<!-- /网校课程 结束 -->
<!-- 网校名师 开始 -->
<div>
<section class="container">
<header class="comm-title">
<h2 class="tac">
<span class="c-333">名师大咖</span>
</h2>
</header>
<div>
<article class="i-teacher-list">
<ul class="of">
<li>
<section class="i-teach-wrap">
<div class="i-teach-pic">
<a href="/teacher/1" title="姚晨">
<img
alt="姚晨"
src="~/assets/photo/teacher/1442297885942.jpg"
/>
</a>
</div>
<div class="mt10 hLh30 txtOf tac">
<a href="/teacher/1" title="姚晨" class="fsize18 c-666"
>姚晨</a
>
</div>
<div class="hLh30 txtOf tac">
<span class="fsize14 c-999"
>北京师范大学法学院副教授</span
>
</div>
<div class="mt15 i-q-txt">
<p class="c-999 f-fA">
北京师范大学法学院副教授、清华大学法学博士。自2004年至今已有9年的司法考试培训经验。长期从事司法考试辅导,深知命题规律,了解解题技巧。内容把握准确,授课重点明确,层次分明,调理清晰,将法条法理与案例有机融合,强调综合,深入浅出。
</p>
</div>
</section>
</li>
<li>
<section class="i-teach-wrap">
<div class="i-teach-pic">
<a href="/teacher/1" title="谢娜">
<img
alt="谢娜"
src="~/assets/photo/teacher/1442297919077.jpg"
/>
</a>
</div>
<div class="mt10 hLh30 txtOf tac">
<a href="/teacher/1" title="谢娜" class="fsize18 c-666"
>谢娜</a
>
</div>
<div class="hLh30 txtOf tac">
<span class="fsize14 c-999"
>资深课程设计专家,专注10年AACTP美国培训协会认证导师</span
>
</div>
<div class="mt15 i-q-txt">
<p class="c-999 f-fA">
十年课程研发和培训咨询经验,曾任国企人力资源经理、大型外企培训经理,负责企业大学和培训体系搭建;曾任专业培训机构高级顾问、研发部总监,为包括广东移动、东莞移动、深圳移动、南方电网、工商银行、农业银行、民生银行、邮储银行、TCL集团、清华大学继续教育学院、中天路桥、广西扬翔股份等超过200家企业提供过培训与咨询服务,并担任近50个大型项目的总负责人。
</p>
</div>
</section>
</li>
<li>
<section class="i-teach-wrap">
<div class="i-teach-pic">
<a href="/teacher/1" title="刘德华">
<img
alt="刘德华"
src="~/assets/photo/teacher/1442297927029.jpg"
/>
</a>
</div>
<div class="mt10 hLh30 txtOf tac">
<a href="/teacher/1" title="刘德华" class="fsize18 c-666"
>刘德华</a
>
</div>
<div class="hLh30 txtOf tac">
<span class="fsize14 c-999"
>上海师范大学法学院副教授</span
>
</div>
<div class="mt15 i-q-txt">
<p class="c-999 f-fA">
上海师范大学法学院副教授、清华大学法学博士。自2004年至今已有9年的司法考试培训经验。长期从事司法考试辅导,深知命题规律,了解解题技巧。内容把握准确,授课重点明确,层次分明,调理清晰,将法条法理与案例有机融合,强调综合,深入浅出。
</p>
</div>
</section>
</li>
<li>
<section class="i-teach-wrap">
<div class="i-teach-pic">
<a href="/teacher/1" title="周润发">
<img
alt="周润发"
src="~/assets/photo/teacher/1442297935589.jpg"
/>
</a>
</div>
<div class="mt10 hLh30 txtOf tac">
<a href="/teacher/1" title="周润发" class="fsize18 c-666"
>周润发</a
>
</div>
<div class="hLh30 txtOf tac">
<span class="fsize14 c-999"
>考研政治辅导实战派专家,全国考研政治命题研究组核心成员。</span
>
</div>
<div class="mt15 i-q-txt">
<p class="c-999 f-fA">
法学博士,北京师范大学马克思主义学院副教授,专攻毛泽东思想概论、邓小平理论,长期从事考研辅导。出版著作两部,发表学术论文30余篇,主持国家社会科学基金项目和教育部重大课题子课题各一项,参与中央实施马克思主义理论研究和建设工程项目。
</p>
</div>
</section>
</li>
</ul>
<div class="clear"></div>
</article>
<section class="tac pt20">
<a href="#" title="全部讲师" class="comm-btn c-btn-2">全部讲师</a>
</section>
</div>
</section>
</div>
<!-- /网校名师 结束 -->
</div>
</div>
</template>
<script>
export default {
data () {
return {
swiperOption: {
//配置分页
pagination: {
el: '.swiper-pagination'//分页的dom节点
},
//配置导航
navigation: {
nextEl: '.swiper-button-next',//下一页dom节点
prevEl: '.swiper-button-prev',//前一页dom节点
},
// 轮播图自动播放
autoplay: {
delay: 2000,
},
speed: 800,
}
}
}
}
</script>
如果出现以下错误,说明版本不一致,解决办法就是将 node_modules 和 package-lock.json 删掉,重新
npm install
三、NUXT 中的路由
1.固定路由
路径是固定地址,不发生变化
使用 标签实现跳转, to: 跳转的地址
在 pages 中创建 course 文件夹,文件夹下创建 index.vue 页面,点击 课程 就会跳转到 /pages/course/index.vue 页面
/pages/course/index.vue 课程页面静态模板:
<template>
<div id="aCoursesList" class="bg-fa of">
<!-- /课程列表 开始 -->
<section class="container">
<header class="comm-title">
<h2 class="fl tac">
<span class="c-333">全部课程</span>
</h2>
</header>
<section class="c-sort-box">
<section class="c-s-dl">
<dl>
<dt>
<span class="c-999 fsize14">课程类别</span>
</dt>
<dd class="c-s-dl-li">
<ul class="clearfix">
<li>
<a title="全部" href="#">全部</a>
</li>
<li>
<a title="数据库" href="#">数据库</a>
</li>
<li class="current">
<a title="外语考试" href="#">外语考试</a>
</li>
<li>
<a title="教师资格证" href="#">教师资格证</a>
</li>
<li>
<a title="公务员" href="#">公务员</a>
</li>
<li>
<a title="移动开发" href="#">移动开发</a>
</li>
<li>
<a title="操作系统" href="#">操作系统</a>
</li>
</ul>
</dd>
</dl>
<dl>
<dt>
<span class="c-999 fsize14"></span>
</dt>
<dd class="c-s-dl-li">
<ul class="clearfix">
<li>
<a title="职称英语" href="#">职称英语</a>
</li>
<li>
<a title="英语四级" href="#">英语四级</a>
</li>
<li>
<a title="英语六级" href="#">英语六级</a>
</li>
</ul>
</dd>
</dl>
<div class="clear"></div>
</section>
<div class="js-wrap">
<section class="fr">
<span class="c-ccc">
<i class="c-master f-fM">1</i>/
<i class="c-666 f-fM">1</i>
</span>
</section>
<section class="fl">
<ol class="js-tap clearfix">
<li>
<a title="关注度" href="#">关注度</a>
</li>
<li>
<a title="最新" href="#">最新</a>
</li>
<li class="current bg-orange">
<a title="价格" href="#">价格
<span>↓</span>
</a>
</li>
</ol>
</section>
</div>
<div class="mt40">
<!-- /无数据提示 开始-->
<section class="no-data-wrap">
<em class="icon30 no-data-ico"> </em>
<span class="c-666 fsize14 ml10 vam">没有相关数据,小编正在努力整理中...</span>
</section>
<!-- /无数据提示 结束-->
<article class="comm-course-list">
<ul class="of" id="bna">
<li>
<div class="cc-l-wrap">
<section class="course-img">
<img src="~/assets/photo/course/1442295592705.jpg" class="img-responsive" alt="听力口语">
<div class="cc-mask">
<a href="/course/1" title="开始学习" class="comm-btn c-btn-1">开始学习</a>
</div>
</section>
<h3 class="hLh30 txtOf mt10">
<a href="/course/1" title="听力口语" class="course-title fsize18 c-333">听力口语</a>
</h3>
<section class="mt10 hLh20 of">
<span class="fr jgTag bg-green">
<i class="c-fff fsize12 f-fA">免费</i>
</span>
<span class="fl jgAttr c-ccc f-fA">
<i class="c-999 f-fA">9634人学习</i>
|
<i class="c-999 f-fA">9634评论</i>
</span>
</section>
</div>
</li>
<li>
<div class="cc-l-wrap">
<section class="course-img">
<img src="~/assets/photo/course/1442295581911.jpg" class="img-responsive" alt="Java精品课程">
<div class="cc-mask">
<a href="/course/1" title="开始学习" class="comm-btn c-btn-1">开始学习</a>
</div>
</section>
<h3 class="hLh30 txtOf mt10">
<a href="/course/1" title="Java精品课程" class="course-title fsize18 c-333">Java精品课程</a>
</h3>
<section class="mt10 hLh20 of">
<span class="fr jgTag bg-green">
<i class="c-fff fsize12 f-fA">免费</i>
</span>
<span class="fl jgAttr c-ccc f-fA">
<i class="c-999 f-fA">501人学习</i>
|
<i class="c-999 f-fA">501评论</i>
</span>
</section>
</div>
</li>
<li>
<div class="cc-l-wrap">
<section class="course-img">
<img src="~/assets/photo/course/1442295604295.jpg" class="img-responsive" alt="C4D零基础">
<div class="cc-mask">
<a href="/course/1" title="开始学习" class="comm-btn c-btn-1">开始学习</a>
</div>
</section>
<h3 class="hLh30 txtOf mt10">
<a href="/course/1" title="C4D零基础" class="course-title fsize18 c-333">C4D零基础</a>
</h3>
<section class="mt10 hLh20 of">
<span class="fr jgTag bg-green">
<i class="c-fff fsize12 f-fA">免费</i>
</span>
<span class="fl jgAttr c-ccc f-fA">
<i class="c-999 f-fA">300人学习</i>
|
<i class="c-999 f-fA">300评论</i>
</span>
</section>
</div>
</li>
<li>
<div class="cc-l-wrap">
<section class="course-img">
<img
src="~/assets/photo/course/1442302831779.jpg"
class="img-responsive"
alt="数学给宝宝带来的兴趣"
>
<div class="cc-mask">
<a href="/course/1" title="开始学习" class="comm-btn c-btn-1">开始学习</a>
</div>
</section>
<h3 class="hLh30 txtOf mt10">
<a href="/course/1" title="数学给宝宝带来的兴趣" class="course-title fsize18 c-333">数学给宝宝带来的兴趣</a>
</h3>
<section class="mt10 hLh20 of">
<span class="fr jgTag bg-green">
<i class="c-fff fsize12 f-fA">免费</i>
</span>
<span class="fl jgAttr c-ccc f-fA">
<i class="c-999 f-fA">256人学习</i>
|
<i class="c-999 f-fA">256评论</i>
</span>
</section>
</div>
</li>
<li>
<div class="cc-l-wrap">
<section class="course-img">
<img
src="~/assets/photo/course/1442295455437.jpg"
class="img-responsive"
alt="零基础入门学习Python课程学习"
>
<div class="cc-mask">
<a href="/course/1" title="开始学习" class="comm-btn c-btn-1">开始学习</a>
</div>
</section>
<h3 class="hLh30 txtOf mt10">
<a
href="/course/1"
title="零基础入门学习Python课程学习"
class="course-title fsize18 c-333"
>零基础入门学习Python课程学习</a>
</h3>
<section class="mt10 hLh20 of">
<span class="fr jgTag bg-green">
<i class="c-fff fsize12 f-fA">免费</i>
</span>
<span class="fl jgAttr c-ccc f-fA">
<i class="c-999 f-fA">137人学习</i>
|
<i class="c-999 f-fA">137评论</i>
</span>
</section>
</div>
</li>
<li>
<div class="cc-l-wrap">
<section class="course-img">
<img
src="~/assets/photo/course/1442295570359.jpg"
class="img-responsive"
alt="MySql从入门到精通"
>
<div class="cc-mask">
<a href="/course/1" title="开始学习" class="comm-btn c-btn-1">开始学习</a>
</div>
</section>
<h3 class="hLh30 txtOf mt10">
<a href="/course/1" title="MySql从入门到精通" class="course-title fsize18 c-333">MySql从入门到精通</a>
</h3>
<section class="mt10 hLh20 of">
<span class="fr jgTag bg-green">
<i class="c-fff fsize12 f-fA">免费</i>
</span>
<span class="fl jgAttr c-ccc f-fA">
<i class="c-999 f-fA">125人学习</i>
|
<i class="c-999 f-fA">125评论</i>
</span>
</section>
</div>
</li>
<li>
<div class="cc-l-wrap">
<section class="course-img">
<img src="~/assets/photo/course/1442302852837.jpg" class="img-responsive" alt="搜索引擎优化技术">
<div class="cc-mask">
<a href="/course/1" title="开始学习" class="comm-btn c-btn-1">开始学习</a>
</div>
</section>
<h3 class="hLh30 txtOf mt10">
<a href="/course/1" title="搜索引擎优化技术" class="course-title fsize18 c-333">搜索引擎优化技术</a>
</h3>
<section class="mt10 hLh20 of">
<span class="fr jgTag bg-green">
<i class="c-fff fsize12 f-fA">免费</i>
</span>
<span class="fl jgAttr c-ccc f-fA">
<i class="c-999 f-fA">123人学习</i>
|
<i class="c-999 f-fA">123评论</i>
</span>
</section>
</div>
</li>
<li>
<div class="cc-l-wrap">
<section class="course-img">
<img src="~/assets/photo/course/1442295379715.jpg" class="img-responsive" alt="20世纪西方音乐">
<div class="cc-mask">
<a href="/course/1" title="开始学习" class="comm-btn c-btn-1">开始学习</a>
</div>
</section>
<h3 class="hLh30 txtOf mt10">
<a href="/course/1" title="20世纪西方音乐" class="course-title fsize18 c-333">20世纪西方音乐</a>
</h3>
<section class="mt10 hLh20 of">
<span class="fr jgTag bg-green">
<i class="c-fff fsize12 f-fA">免费</i>
</span>
<span class="fl jgAttr c-ccc f-fA">
<i class="c-999 f-fA">34人学习</i>
|
<i class="c-999 f-fA">34评论</i>
</span>
</section>
</div>
</li>
</ul>
<div class="clear"></div>
</article>
</div>
<!-- 公共分页 开始 -->
<div>
<div class="paging">
<a class="undisable" title>首</a>
<a id="backpage" class="undisable" href="#" title><</a>
<a href="#" title class="current undisable">1</a>
<a href="#" title>2</a>
<a id="nextpage" href="#" title>></a>
<a href="#" title>末</a>
<div class="clear"></div>
</div>
</div>
<!-- 公共分页 结束 -->
</section>
</section>
<!-- /课程列表 结束 -->
</div>
</template>
<script>
export default {};
</script>
2.动态路由
路径是变化的,比如课程详情页面, 根据 ID 查询课程信息,不同的 ID 页面信息是不一样的。
在 NUXT 中 动态路由的固定写法: _id.vue
必须以下划线开头,名字无所谓,但是最好见名知意。
/pages/course/_id.vue 课程详情页面静态模板:
<template>
<div id="aCoursesList" class="bg-fa of">
<!-- /课程详情 开始 -->
<section class="container">
<section class="path-wrap txtOf hLh30">
<a href="#" title class="c-999 fsize14">首页</a>
\
<a href="#" title class="c-999 fsize14">课程列表</a>
\
<span class="c-333 fsize14">Java精品课程</span>
</section>
<div>
<article class="c-v-pic-wrap" style="height: 357px;">
<section class="p-h-video-box" id="videoPlay">
<img src="~/assets/photo/course/1442295581911.jpg" alt="Java精品课程" class="dis c-v-pic">
</section>
</article>
<aside class="c-attr-wrap">
<section class="ml20 mr15">
<h2 class="hLh30 txtOf mt15">
<span class="c-fff fsize24">Java精品课程</span>
</h2>
<section class="c-attr-jg">
<span class="c-fff">价格:</span>
<b class="c-yellow" style="font-size:24px;">¥0.00</b>
</section>
<section class="c-attr-mt c-attr-undis">
<span class="c-fff fsize14">主讲: 唐嫣 </span>
</section>
<section class="c-attr-mt of">
<span class="ml10 vam">
<em class="icon18 scIcon"></em>
<a class="c-fff vam" title="收藏" href="#" >收藏</a>
</span>
</section>
<section class="c-attr-mt">
<a href="#" title="立即观看" class="comm-btn c-btn-3">立即观看</a>
</section>
</section>
</aside>
<aside class="thr-attr-box">
<ol class="thr-attr-ol clearfix">
<li>
<p> </p>
<aside>
<span class="c-fff f-fM">购买数</span>
<br>
<h6 class="c-fff f-fM mt10">150</h6>
</aside>
</li>
<li>
<p> </p>
<aside>
<span class="c-fff f-fM">课时数</span>
<br>
<h6 class="c-fff f-fM mt10">20</h6>
</aside>
</li>
<li>
<p> </p>
<aside>
<span class="c-fff f-fM">浏览数</span>
<br>
<h6 class="c-fff f-fM mt10">501</h6>
</aside>
</li>
</ol>
</aside>
<div class="clear"></div>
</div>
<!-- /课程封面介绍 -->
<div class="mt20 c-infor-box">
<article class="fl col-7">
<section class="mr30">
<div class="i-box">
<div>
<section id="c-i-tabTitle" class="c-infor-tabTitle c-tab-title">
<a name="c-i" class="current" title="课程详情">课程详情</a>
</section>
</div>
<article class="ml10 mr10 pt20">
<div>
<h6 class="c-i-content c-infor-title">
<span>课程介绍</span>
</h6>
<div class="course-txt-body-wrap">
<section class="course-txt-body">
<p>
Java的发展历史,可追溯到1990年。当时Sun Microsystem公司为了发展消费性电子产品而进行了一个名为Green的项目计划。该计划
负责人是James Gosling。起初他以C++来写一种内嵌式软件,可以放在烤面包机或PAD等小型电子消费设备里,使得机器更聪明,具有人工智
能。但他发现C++并不适合完成这类任务!因为C++常会有使系统失效的程序错误,尤其是内存管理,需要程序设计师记录并管理内存资源。这给设计师们造成
极大的负担,并可能产生许多bugs。
<br>为了解决所遇到的问题,Gosling决定要发展一种新的语言,来解决C++的潜在性危险问题,这个语言名叫Oak。Oak是一种可移植性语言,也就是一种平台独立语言,能够在各种芯片上运行。
<br>1994年,Oak技术日趋成熟,这时网络正开始蓬勃发展。Oak研发小组发现Oak很适合作为一种网络程序语言。因此发展了一个能与Oak配合的浏
览器--WebRunner,后更名为HotJava,它证明了Oak是一种能在网络上发展的程序语言。由于Oak商标已被注册,工程师们便想到以自己常
享用的咖啡(Java)来重新命名,并于Sun World 95中被发表出来。
</p>
</section>
</div>
</div>
<!-- /课程介绍 -->
<div class="mt50">
<h6 class="c-g-content c-infor-title">
<span>课程大纲</span>
</h6>
<section class="mt20">
<div class="lh-menu-wrap">
<menu id="lh-menu" class="lh-menu mt10 mr10">
<ul>
<!-- 文件目录 -->
<li class="lh-menu-stair">
<a href="javascript: void(0)" title="第一章" class="current-1">
<em class="lh-menu-i-1 icon18 mr10"></em>第一章
</a>
<ol class="lh-menu-ol" style="display: block;">
<li class="lh-menu-second ml30">
<a href="#" title>
<span class="fr">
<i class="free-icon vam mr10">免费试听</i>
</span>
<em class="lh-menu-i-2 icon16 mr5"> </em>第一节
</a>
</li>
<li class="lh-menu-second ml30">
<a href="#" title class="current-2">
<em class="lh-menu-i-2 icon16 mr5"> </em>第二节
</a>
</li>
</ol>
</li>
</ul>
</menu>
</div>
</section>
</div>
<!-- /课程大纲 -->
</article>
</div>
</section>
</article>
<aside class="fl col-3">
<div class="i-box">
<div>
<section class="c-infor-tabTitle c-tab-title">
<a title href="javascript:void(0)">主讲讲师</a>
</section>
<section class="stud-act-list">
<ul style="height: auto;">
<li>
<div class="u-face">
<a href="#">
<img src="~/assets/photo/teacher/1442297969808.jpg" width="50" height="50" alt>
</a>
</div>
<section class="hLh30 txtOf">
<a class="c-333 fsize16 fl" href="#">周杰伦</a>
</section>
<section class="hLh20 txtOf">
<span class="c-999">毕业于北京大学数学系</span>
</section>
</li>
</ul>
</section>
</div>
</div>
</aside>
<div class="clear"></div>
</div>
</section>
<!-- /课程详情 结束 -->
</div>
</template>
<script>
export default {};
</script>
四、封装 axios
- 安装 axios
npm install axios
- 创建 utils 目录,目录下创建 request.js 文件,封装 axios
import axios from 'axios'
// 创建axios实例
const service = axios.create({
baseURL: 'http://192.168.200.132:9003', // api的base_url
timeout: 20000 // 请求超时时间
})
export default service
五、Redis
redis 学习笔记:https://blog.csdn.net/aetawt/article/details/126105301
SpringBoot 缓存注解介绍:
@Cacheable
根据方法对其返回结果进行缓存,下次请求时,如果缓存存在,则直接读取缓存数据返回;如果缓存不存在,则执行方法,并把返回的结果存入缓存中。一般用在查询方法上
。
属性/方法名 | 解释 |
---|---|
value | 缓存名,必填,它指定了你的缓存存放在哪块命名空间 |
cacheNames | 与 value 差不多,二选一即可 |
key | 可选属性,可以使用 SpEL 标签自定义缓存的key |
@Cacheable 注解的流程:
该注解是将方法返回值保存到缓存中,一般不建议在 controller 中用,一般都是在 serviceImpl 中使用该注解。
@CachePut
使用该注解标志的方法,每次都会执行,并将结果存入指定的缓存中。其他方法可以直接从响应的缓存中读取缓存数据,而不需要再去查询数据库。一般用在新增方法上
属性/方法名 | 解释 |
---|---|
value | 缓存名,必填,它指定了你的缓存存放在哪块命名空间 |
cacheNames | 与 value 差不多,二选一即可 |
key | 可选属性,可以使用 SpEL 标签自定义缓存的key |
@CacheEvict
使用该注解标志的方法,会清空指定的缓存。一般用在更新或者删除方法上
属性/方法名 | 解释 |
---|---|
value | 缓存名,必填,它指定了你的缓存存放在哪块命名空间 |
cacheNames | 与 value 差不多,二选一即可 |
key | 可选属性,可以使用 SpEL 标签自定义缓存的key |
allEntries | 是否清空所有缓存 ,默认为 false。如果指定为 true,则方法调用后将立即清空所有的缓存 |
beforeInvocation | 是否在方法执行前就清空,默认为 false。如果指定为 true,则在方法执行前就会清空缓存 |
启动 Redis:
- 修改 redis.conf 配置文件
- 注释掉:#bind 127.0.0.1 -::1
- 关闭保护模式: protected-mode no
- 开启后台启动:daemonize yes
- 通过配置文件启动
redis-server //usr/local/redis/redis-6.2.1/redis.conf
- 进入命令行客户端:
redis-cli -p 6379
其他 Redis 操作,请查看 :https://blog.csdn.net/aetawt/article/details/126105301
1.将banner存入 redis 缓存
项目整合 Redis :
- 在其他模块也需要用 Redis,因此在 common 模块中引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- spring2.X集成redis所需common-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
- 在 common_base 模块中增加序列化配置文件
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
// 序列化方式
// RedisConnectionFactory 连接工厂,自动从 IOC 容器中获取
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 配置连接工厂
template.setConnectionFactory(factory);
// 使用 jackso 序列化方式
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
//key序列化方式
template.setKeySerializer(new StringRedisSerializer());
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
- 在 service_cms 模块中增加 redis 相关配置
# redis
spring.redis.host=192.168.200.132
spring.redis.port=6379
spring.redis.database= 0
spring.redis.timeout=1800000
spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-wait=-1
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-idle=5
spring.redis.lettuce.pool.min-idle=0
- 在 获取 banner图 的方法上增加
@Cacheable
注解
注意: key 的 命名,需要一个
双引号
和单引号
。
- 存入redis后的数据
六、banner 模块
1.环境搭建
- 新建 service_cms 模块
- 配置文件
# 服务端口
server.port=8004
# 服务名
spring.application.name=service_cms
# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/guli?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=1234
#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
- 增加数据库表
- 使用 MyBatis 插件生成代码
实体类中增加 自动填充 注解:
- 启动类
@SpringBootApplication
@MapperScan("com.atguigu.cms.mapper")
@ComponentScan("com.atguigu")
public class CmsMainApplication {
public static void main(String[] args) {
SpringApplication.run(CmsMainApplication.class,args);
}
}
- controller 层中创建俩个 controller,一个用于 后台管理,一个用于前台显示
- Nginx 中配置
Nginx 配置文件增加请求转发路径:
2.功能介绍
需要做的功能:
- 后台对 banner 图的管理
- 显示所有banner图的列表
- 设置前台显示的 banner 图
- 增加 banner 图
- 删除 banner 图
- 前台显示 banner 图
3.后台 banner 管理
(1) 显示 banner 列表
效果:
点击图片可实现预览图片的效果:
后台管理 – 后端设计:
- CmsAdminController 层,分页查询 所有的 banner
@RestController
@RequestMapping("cmsService/cmsAdmin")
@CrossOrigin
public class CmsAdminController {
@Autowired
private CrmBannerService bannerService;
@ApiOperation(value = "获取Banner分页列表")
@GetMapping("pageQuery/{current}/{limit}")
public R index(
@PathVariable Long current,
@PathVariable Long limit) {
Page<CrmBanner> pageParam = new Page<>(current, limit);
bannerService.page(pageParam, null);
return R.ok().data("items", pageParam.getRecords()).data("total", pageParam.getTotal());
}
}
后台管理 – 前端设计:
- vue-admin-1010 中,在 /src/router/index.js 中增加路由
// banner 管理
{
path: '/banner',
component: Layout,
redirect: '/banner/list',
name: 'banner',
meta: { title: 'banner管理', icon: 'example' },
children: [
{
path: 'list',
name: 'banner图列表',
component: () => import('@/views/edu/banner/list'),
meta: { title: 'banner列表', icon: 'table' }
},
{
path: 'save',
name: '增加banner图',
component: () => import('@/views/edu/banner/save'),
meta: { title: '增加banner图', icon: 'tree' }
}
]
},
- 在 /src/views/edu/banner/ 目录下创建 list.vue , save.vue 页面
- 在 /src/api/front 目录下创建 banner.js ,用于定义访问接口的 api
// request 封装了axios
import request from '@/utils/request'
// ES6 模块化
export default {
// 1. 分页获取 banner 数据
getAllBanner(current, limit) {
return request({
url: `cmsService/cmsAdmin/pageQuery/${current}/${limit}`,
method: 'get',
})
},
}
- 在 list.vue 页面定义模板
<template>
<div class="app-container">
<!-- banner列表表格 -->
<el-table :data="list" border fit highlight-current-row>
<el-table-column label="序号" width="70" align="center">
<template slot-scope="scope">
<!-- 计算序号的一个公式 -->
{{ (current - 1) * limit + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column prop="title" label="标题" width="80" />
<!-- 图片 -->
<el-table-column
prop="imageUrl"
label="图片预览"
width="400"
align="center"
>
<!-- 图片预览 -->
<template width="90" slot-scope="scope">
<div class="demo-image__preview">
<el-image
style="width: 200px; height: 100px"
:src="scope.row.imageUrl"
:preview-src-list="previewList"
>
</el-image>
</div>
</template>
</el-table-column>
<el-table-column prop="linkUrl" label="链接" width="100" align="center" />
<el-table-column prop="gmtCreate" label="添加时间" width="160" />
<el-table-column prop="sort" label="排序" width="60" align="center" />
<el-table-column label="操作" align="center">
<template slot-scope="scope">
<el-button
type="primary"
size="mini"
icon="el-icon-edit"
@click="open(scope.row.id)"
>修改</el-button
>
<el-button
type="danger"
size="mini"
icon="el-icon-delete"
@click="removeDataById(scope.row.id)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<!-- Element-ui 会自动将这些值封装 -->
<el-pagination
:current-page="current"
:page-size="limit"
:total="total"
style="padding: 30px 0; text-align: center"
layout="total, prev, pager, next, jumper"
@current-change="getBannerList"
/>
</div>
</template>
:src : 图片源
:preview-src-list : 开启图片预览功能,
参数是一个数组类型
- Js 代码部分
- 引入 banner.js 文件
- data 中定义数据
- created 中调用 方法
- methods 调用 api
<script>
import banner from "@/api/front/banner";
export default {
data() {
return {
list: [], // 保存banner数据
current: 1,
limit: 10,
total: 0,
previewList: [], // 保存预览图数组,预览图必须是一个数组
};
},
created() {
this.getBannerList();
},
methods: {
getBannerList(current = 1) {
banner.getAllBanner(current, this.limit).then((response) => {
this.list = response.data.items;
// items是一个数组,将items数组拷贝到 perviewList
for (var i = 0; i < response.data.items.length; i++) {
// push:是往数组末尾增加数据
this.previewList.push(response.data.items[i].imageUrl);
}
this.total = response.data.total;
});
},
},
};
</script>
如果在控制台报错:
Unknown custom element: el-image
请在 package.json 中 修改 element-ui 的版本为 2.15.7 :
修改完重新安装依赖:
cnpm install
(2) 后台设置前台显示的banner图
效果演示:
点击设置 弹出设置框:
设置框里面的 banner 图,单击有预览效果
里面的 banner 图是根据数据库关联的,勾选上哪个 banner 图,前台对应显示哪个 banner图。
思路分析:
- 首先查询数据库在对话框中显示所有的 banner 图,这其实很简单。
- 使用 js 再点击确定的时候,判断有哪些banner 选中了,并将 这些 banner 图 的 ID 保存到一个集合中。
- 将 ID 集合 传到后端,后端根据这个 ID 集合 查询数据库,保存到 redis 缓存中
- 前台界面先从 redis 取 banner,如果有就直接取,没有查询数据库 ,显示 banner
后台管理 – 后端设计:
- CmsBannerService 层
接口:
在接口中增加一个方法,该方法是后台管理去调用,往 redis 中存数据
// 修改前台banner图的数量
List<CrmBanner> editFrontBannerCount(List<String> bannerIds);
实现类:
在这里 我并没有 @Cacheable 注解了,而是使用了 set、get 来存取 数据
@Service
public class CrmBannerServiceImpl extends ServiceImpl<CrmBannerMapper, CrmBanner>
implements CrmBannerService {
@Autowired
private RedisTemplate redisTemplate;
/**
* @description 修改前台 banner 图的数量
* @date 2022/8/31 14:35
* @param bannerIds
* @return java.util.List<com.atguigu.cms.entity.CrmBanner>
*/
@Override
public List<CrmBanner> editFrontBannerCount(List<String> bannerIds) {
// 创建 list 集合 保存banner 图
List<CrmBanner> list = new ArrayList<>();
for (String bannerId : bannerIds) {
CrmBanner crmBanner = baseMapper.selectById(bannerId);
if (crmBanner != null) {
list.add(crmBanner);
}
}
// 存入 redis,设置永不过期
redisTemplate.opsForValue().set("editBannerList",list);
return list;
}
}
- 后台管理 controller 层
- 注意:使用 @RequestBody 注解时,提交方式必须是 Post 。
@ApiOperation(value = "修改前台banner图")
@PostMapping("editFrontBannerCount")
public R editFrontBannerCount(@RequestBody List<String> bannerIds) {
bannerService.editFrontBannerCount(bannerIds);
return R.ok();
}
后端管理 – 前端设计:
- 在 /src/api/front/banner.js 中定义访问接口的方法
editBannerCount(bannerIds) {
return request({
url: `cmsService/cmsAdmin/editFrontBannerCount/`,
method: 'post',
data: bannerIds
})
}
- 在 /views/edu/banner/list.vue 页面,增加设置按钮 和 弹框
<template>
<div class="app-container">
<!-- 设置显示的轮播图 -->
<el-row>
<el-button
type="info"
plain
class="el-icon-s-tools"
@click="dialogFormVisible = true"
>
设置</el-button
>
</el-row>
<br />
<!-- 设置框 -->
<el-dialog title="设置前台展示的轮播图" :visible.sync="dialogFormVisible" width="30%" top="5vh" @open="openDialog()">
<el-form>
<span v-for="banner in list" :key="banner.id">
<br>
<input type="checkbox" :value="banner.id" name="changedBanner" />
<br>
<!-- 开启预览图 -->
<span class="demo-image__preview">
<el-image
style="width: 320px; height: 120px"
:src="banner.imageUrl"
:preview-src-list="previewList"
>
</el-image>
</span>
</span>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取 消</el-button>
<el-button type="primary" @click="editBanner()">确 定</el-button>
</div>
</el-dialog>
@open 是打开对话框执行的回调函数
- data中定义数据
// 设置轮播图
dialogFormVisible: false,
bannerIds: [], // 保存选中的图片的ID
- methods 编写方法,调用 banner.js 里面的方法
editBanner 方法是点击 对话框
确定
执行的,一共需要做以下几件事:
- 在点击确定时,统计 对话框中勾选的 banner 图,并将 banner图的 ID 保存到集合中
- 提示框,点击确定 调用 banner.js 中的方法
- 重新刷新页面
openDialog 方法时打开对话框执行的回调函数,主要是 清空 banner ID集合,如果不清空,还会保存上次勾选 banner 图的 id。
// 打开对话框 清空 banner集合
openDialog() {
this.bannerIds = [] ;
console.log(this.bannerIds)
},
editBanner() {
// 获取复选框DOM元素
var obj = document.getElementsByName("changedBanner");
for (var i = 0; i < obj.length; i++) {
if (obj[i].checked) {
// 将选中的图片 id 保存到数组中
this.bannerIds.push(obj[i].value);
}
}
console.log(this.bannerIds);
this.$confirm("确定将设置前台banner图吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(() => {
// 关闭对话框
this.dialogFormVisible = false;
// 点击 确定 执行的方法
banner.editBannerCount(this.bannerIds).then((response) => {
// 设置成功的方法
this.$message({
type: "success",
message: "设置成功!",
});
// 刷新页面
this.getBannerList();
});
});
},
(3) 增加 banner 图
增加 banner 图需要做的事:
- 图片上传到 oss
- 图片的地址存到数据库
后端设计:
- CmsAdminController ,增加 banner
@ApiOperation(value = "新增Banner")
@PostMapping("saveBanner")
public R save(@RequestBody CrmBanner banner) {
bannerService.save(banner);
return R.ok();
}
前端设计:
- /src/api/front/banner.js 中定义增加banner 的方法
// 3.增加 banner
addBanner(banner) {
return request({
url: `cmsService/cmsAdmin/saveBanner/`,
method: 'post',
data: banner
})
},
- 增加页面模板
<template>
<div class="app-container">
<el-form label-width="120px">
<el-form-item label="图片标题">
<el-input v-model="banner.title" />
</el-form-item>
<el-form-item label="图片排序">
<el-input-number
v-model="banner.sort"
controls-position="right"
:min="0"
/>
</el-form-item>
<!-- 图片跳转链接 -->
<el-form-item label="图片跳转链接">
<el-select v-model="banner.linkUrl" clearable placeholder="请选择">
<el-option :value="'/course'" label="课程页面" />
<el-option :value="'/teacher'" label="教师页面" />
</el-select>
</el-form-item>
<el-form-item label="上传banner图">
<el-upload
class="avatar-uploader"
:action="BASE_API + '/oss/file/upload'"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
>
<img v-if="banner.imageUrl" :src="banner.imageUrl" class="avatar" />
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</el-form-item>
<el-form-item>
<el-button
:disabled="saveBtnDisabled"
type="primary"
@click="saveBanner"
>增加</el-button
>
</el-form-item>
</el-form>
</div>
</template>
- js 代码
- 引入 banner.js 文件
- data 中定义需要的数据
- methods 定义需要的方法
- 调用 banner.js 方法,实现增加banner
- handleAvatarSuccess 上传成功执行的方法,主要是获取图片的 url
- beforeAvatarUpload 上传前执行的方法,主要是定义上传图片的类型和大小
<script>
import banner from "@/api/front/banner";
export default {
data() {
return {
banner: {
// 这里必须定义 imageURl,别的属性可以不定义
// 如果不定义,上传完之后不会显示上传的图片
imageUrl:''
},
BASE_API: process.env.BASE_API, // 接口API地址
saveBtnDisabled: false
};
},
methods: {
saveBanner(){
banner.addBanner(this.banner).then(response => {
this.$message({
type: "success",
message: "增加成功!",
});
// 增加完跳转 banner 列表
this.$router.push({path: '/banner/list'})
})
},
// 上传成功执行的方法
handleAvatarSuccess(response) {
// 上传成功之后,返回图片的url
this.banner.imageUrl = response.data.url;
console.log(this.banner.imageUrl);
},
// 上传前执行的方法
beforeAvatarUpload(file) {
const isLt2M = file.size / 1024 / 1024 < 5;
if (!isLt2M) {
this.$message.error("上传头像图片大小不能超过 5MB!");
}
return isLt2M;
},
},
};
</script>
- 样式
<style>
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .el-upload:hover {
border-color: #409eff;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
line-height: 178px;
text-align: center;
}
.avatar {
width: 400px;
height: 200px;
display: block;
}
</style>
(4) 删除 banner 图
后端设计:
- CMSAdminController 层,删除方法
@ApiOperation(value = "删除Banner")
@DeleteMapping("removeBanner/{id}")
public R remove(@PathVariable String id) {
bannerService.removeById(id);
return R.ok();
}
前端设计:
- banner.js 中增加方法,
// 4.删除 banner
deleteBanner(bannerId) {
return request({
url: `cmsService/cmsAdmin/removeBanner/` + bannerId,
method: 'delete',
})
},
- methods 中调用 banner.js 中的方法
removeDataById(bannerId) {
this.$confirm("此操作将永久删除banner, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
// 点击 确定 执行的方法
banner.deleteBanner(bannerId).then(response => {
// 删除成功的方法
this.$message({
type: "success",
message: "删除成功!",
});
// 删除后重新查询banner列表
this.getBannerList();
});
})
},
4.前台显示 banner
在 前台中显示的 banner 图,是根据 后台管理 设置而来的,如果没有设置,那就好说了,直接查询数据库显示所有的 banner,或者 显示几个都行,自己定。
判断后台是否设置了 banner 图,只需要从 redis 取值是否 为空,空肯定没有设置,不为空就取出 值。
前台显示 – 后端设计:
- cmsFrontController 层,查询 显示的 banner
@RestController
@RequestMapping("cmsService/cmsFront")
@CrossOrigin
public class CmsFrontController {
@Autowired
private CrmBannerService bannerService;
@ApiOperation(value = "获取首页banner")
@GetMapping("getAllBanner")
public R index() {
List<CrmBanner> list = bannerService.selectIndexList();
return R.ok().data("bannerList", list);
}
}
- CmsBannerService 层
接口:
// 前台显示 banner 图
List<CrmBanner> selectIndexList();
实现类:
@Override
public List<CrmBanner> selectIndexList() {
List<CrmBanner> crmBanners = new ArrayList<>();
// 从 redis 取出
crmBanners = (List<CrmBanner>) redisTemplate.opsForValue().get("editBannerList");
if (null == crmBanners) {
// 说明没有设置banner图
crmBanners = baseMapper.selectList(null);
}
return crmBanners;
}
前台显示 – 前台设计:
在 vue-front-1010 项目中,调用后端接口方法和 在 vue-admin-1010中一样,只不过有些目录需要自己创建
- 创建 api 目录,目录下创建 banner.js 文件,用于访问接口
调用接口的方式 和后台管理系统中 一模一样
import request from '@/utils/request'
export default {
// 1.获取 banner
getList() {
return request({
url: `/cmsService/cmsFront/getAllBanner`,
method: 'get'
})
}
}
- 在 /pages/index.vue 中,data中定义数据
// 保存 banner 图像
bannerList: [],
- methods 中定义方法
methods: {
getBannerList() {
banner.getList().then((response) => {
// 这里和写后台管理不一样, 需要俩次 .data
// 因为后台管理中,帮我们封装了一次 .data
console.log(response.data.data.bannerList)
this.bannerList = response.data.data.bannerList
})
},
}
- created 中调用
created() {
this.getBannerList()
},
- 幻灯片中使用 v-for 循环
<!-- 遍历 bannerList -->
<div v-for="banner in bannerList" :key="banner.id" class="swiper-slide" style="background: #040b1b">
<a target="_blank" :href="banner.linkUrl">
<img
:src="banner.imageUrl"
:alt="banner.title"
/>
</a>
</div>
- 增加样式
<style scoped>
.img{
width: 100%;
height: 100%;
}
</style>
src:图片地址,我是将图片上传到 oss 中了,然后将图片地址保存到数据库中
href : 点击图片跳转的路径
七、前台课程,讲师列表显示
1.后端
在主页上,显示 8 条热门课程,和 4 名讲师,根据 ID 进行降序,使用 limit 限制个数。
并且查询来的课程应该是已发布的课程,判断 数据库中 status 字段就行,Normal 为已发布
在 service_edu 中 创建专门 编写 前台 controller 的包:
@RestController
@RequestMapping("/eduservice/front")
@CrossOrigin
public class IndexController {
@Autowired
private EduCourseService eduCourseService;
@Autowired
private EduTeacherService eduTeacherService;
/**
* @description 查询前 8 条热门课程,前 4 条热门讲师
* @date 2022/8/29 15:32
* @param
* @return com.atguigu.commonutils.R
*/
@GetMapping("index")
private R getData() {
// 查询课程
QueryWrapper<EduCourse> courseQueryWrapper = new QueryWrapper<>();
// 根据 ID 降序
courseQueryWrapper.orderByDesc("id");
// last 可以在 后面拼接 sql 语句
courseQueryWrapper.last("limit 8")
// 显示已发布的课程
courseQueryWrapper.eq("status","Normal");
List<EduCourse> eduCourseList = eduCourseService.list(courseQueryWrapper);
// 查询讲师
QueryWrapper<EduTeacher> teacherQueryWrapper = new QueryWrapper<>();
// 根据 ID 降序
teacherQueryWrapper.orderByDesc("id");
// last 可以在 后面拼接 sql 语句
teacherQueryWrapper.last("limit 4");
List<EduTeacher> eduTeacherList = eduTeacherService.list(teacherQueryWrapper);
return R.ok().data("courseList",eduCourseList).data("teacherList",eduTeacherList);
}
}
2.前端
- 在 api 目录下创建 course.js 文件定义访问接口的方法
import request from '@/utils/request'
export default {
// 1.获取 课程 和 教师 列表
getCourseTeacherList() {
return request({
url: `/eduservice/front/index`,
method: 'get'
})
}
}
- 在 /pages/index.vue 页面,调用接口方法
(1) data 中定义数据
courseList:[], // 课程列表
teacherList:[], // 教师列表
(2). methods 中调用 api 方法,返回 后端 的数据
// 获取课程 和 讲师列表
getCourseTeacher() {
course.getCourseTeacherList().then(response => {
this.courseList = response.data.data.courseList
this.teacherList = response.data.data.teacherList
})
},
(3). created 中调用methods 中的方法
this.getCourseTeacher()
(4). 页面使用 v-for 遍历
删除 多余的 li 标签, 只留一个 使用 v-for 循环遍历
插值语法只使用在 标签外部,标签内部使用 表达式应在 属性前加一个 :
<!-- 网校课程 开始 -->
<div>
<section class="container">
<header class="comm-title">
<h2 class="tac">
<span class="c-333">热门课程</span>
</h2>
</header>
<div>
<article class="comm-course-list">
<ul class="of" id="bna">
<li v-for="course in courseList" :key="course.id">
<div class="cc-l-wrap">
<section class="course-img">
<img
:src="course.cover"
class="img-responsive"
:alt="course.title"
/>
<div class="cc-mask">
<a href="#" title="开始学习" class="comm-btn c-btn-1"
>开始学习</a
>
</div>
</section>
<h3 class="hLh30 txtOf mt10">
<a
href="#"
:title="course.title"
class="course-title fsize18 c-333"
>{{ course.title }}</a
>
</h3>
<section class="mt10 hLh20 of">
<span
class="fr jgTag bg-green"
v-if="Number(course.price) === 0"
>
<i class="c-fff fsize12 f-fA">免费</i>
</span>
<span class="fr jgTag bg-green" v-else>
<i class="c-fff fsize12 f-fA"> ¥{{ course.price }}</i>
</span>
<span class="fl jgAttr c-ccc f-fA">
<i class="c-999 f-fA">{{course.buyCount}}人学习</i>
|
<i class="c-999 f-fA">{{course.viewCount}}浏览</i>
</span>
</section>
</div>
</li>
</ul>
<div class="clear"></div>
</article>
<section class="tac pt20">
<a href="#" title="全部课程" class="comm-btn c-btn-2">全部课程</a>
</section>
</div>
</section>
</div>
<!-- /网校课程 结束 -->
<!-- 网校名师 开始 -->
<div>
<section class="container">
<header class="comm-title">
<h2 class="tac">
<span class="c-333">名师大咖</span>
</h2>
</header>
<div>
<article class="i-teacher-list">
<ul class="of">
<li v-for="teacher in teacherList" :key="teacher.id">
<section class="i-teach-wrap">
<div class="i-teach-pic">
<a href="/teacher/1" :title="teacher.name">
<img
:alt="teacher.name"
:src="teacher.avatar"
/>
</a>
</div>
<div class="mt10 hLh30 txtOf tac">
<a href="/teacher/1" :title="teacher.name" class="fsize18 c-666"
>{{teacher.name}}</a
>
</div>
<div class="hLh30 txtOf tac">
<span class="fsize14 c-999"
>{{teacher.career}}</span
>
</div>
<div class="mt15 i-q-txt">
<p class="c-999 f-fA">
{{teacher.intro}}
</p>
</div>
</section>
</li>
</ul>
<div class="clear"></div>
</article>
<section class="tac pt20">
<a href="#" title="全部讲师" class="comm-btn c-btn-2">全部讲师</a>
</section>
</div>
</section>
</div>
<!-- /网校名师 结束 -->
八、前台登录
登录方式介绍:
单一服务器普通的登录方式:
这样的登录方式,是在单一服务器上,但对于如今都是 分布式架构的项目,这种登录方式,就不行了。
对于微服务架构的项目,就不在使用 单一服务器 的等方式,而是一种新的登录技术 —— 单点登录 【SSO(single sign on)模式】
单点登录常见的三种方式:
- session 广播机制实现
- 就是 session 复制,将登录信息保存到 session中,并将 session 中的信息复制到每一个模块中
- 这种方式目前也不怎么用了,主要是模块多了,浪费资源
- cookie + redis 实现
- cookie的特点: 基于客户端的存储技术,有了 cookie 之后,每次发送请求都会将 cookie 发送给服务端
- 在其中一个模块登陆之后,将
登录信息
作为 redis 的value,使用唯一标识(用户id,uuid,时间戳等等)
作为 key 保存到 redis 中- 将 redis 的
key 作为 cookie 的 value
存到客户端,每次发送请求都会带着 cookie,在其它模块中拿着 cookie 的 value 去 redis 中查询值,有信息就是登录,没有就是未登录- 使用 token 实现
- token: 就是一条字符串,字符串里包含着用户信息
- 在其中一个模块进行登录,按照规则生成字符串,将用户信息保存到 字符串中 并返回,返回有俩种方式:
- 存到 cookie 中
- 放到地址栏上进行拼接,类似图片这样:
- 在其他模块中,通过地址栏上取到字符串(cookie),判断该字符串里是否有登录信息,有就是登录,没有就是未登录
1.JWT 介绍
在上面说了,token 是按照一定规则 生成的字符串,而 JWT 就是生成 token 字符串的 一种官方定义的规则。
下面是按照 JWT生成的字符串:
三种颜色分别对应三部分,用 . 分割:
JWT 头信息,JSON 格式
{ "alg": "HS256", // 加密的算法 "typ": "JWT" // 令牌的类型,统一 JWT }
有效载荷部分(用户信息),是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据,JWT指定七个默认字段供选择。
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT除以上默认字段外,我们还可以自定义私有字段,如下例: ```json { "sub": "1234567890", "name": "Helen", "admin": true }
签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改 (防伪标志)。
- 首先,需要指定一个secret(秘钥,每个公司都不一样,自定义)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(claims), secret)
2.项目整合 JWT
- 在 common 模块中引入 JWT 依赖
<dependencies>
<!-- JWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
</dependencies>
- 创建 JwtUtils 工具类
对我们来说所需要修改的地方:
- 过期时间
- 秘钥
- Jwt 主体内容
public class JwtUtils {
// 设置过期时间
public static final long EXPIRE = 1000 * 60 * 60 * 24;
// 生成哈希签名的秘钥
public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";
/**
* @description 获取 token 字符串
* @date 2022/9/1 17:24
* @param id 用户 id
* @param nickname 用户名
* @return java.lang.String
*/
public static String getJwtToken(String id, String nickname) {
String JwtToken = Jwts.builder()
// 设置 jwt 头部信息【令牌类型,加密算法】
.setHeaderParam("typ", "JWT")
.setHeaderParam("alg", "HS256")
// 设置 主体内容,【主题,过期时间,用户信息】
.setSubject("guli-user")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
.claim("id", id)
.claim("nickname", nickname)
// 设置哈西签名
.signWith(SignatureAlgorithm.HS256, APP_SECRET)
.compact();
return JwtToken;
}
/**
* @description 判断 token 字符串是否并在并合理
* @date 2022/9/1 17:26
* @param jwtToken
* @return boolean
*/
public static boolean checkToken(String jwtToken) {
if (StringUtils.isEmpty(jwtToken)) return false;
try {
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* @description 根据 request 对象,判断 token 是否存在和 合理
* @date 2022/9/1 17:27
* @param request
* @return boolean
*/
public static boolean checkToken(HttpServletRequest request) {
try {
String jwtToken = request.getHeader("token");
if (StringUtils.isEmpty(jwtToken)) return false;
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* @description 获取用户 id
* @date 2022/9/1 17:27
* @param request
* @return java.lang.String
*/
public static String getMemberIdByJwtToken(HttpServletRequest request) {
String jwtToken = request.getHeader("token");
if (StringUtils.isEmpty(jwtToken)) return "";
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
Claims claims = claimsJws.getBody();
return (String) claims.get("id");
}
}
3.阿里云短信服务
(1) 环境搭建
- 创建 service_msm 模块
- application 配置文件
# 服务端口
server.port=8006
# 服务名
spring.application.name=service_msm
# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/guli?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=1234
spring.redis.host=192.168.200.132
spring.redis.port=6379
spring.redis.database= 0
spring.redis.timeout=1800000
spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-wait=-1
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-idle=5
spring.redis.lettuce.pool.min-idle=0
#最小空闲
#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
- 启动类
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@ComponentScan("com.atguigu")
public class MsmApplication {
public static void main(String[] args) {
SpringApplication.run(MsmApplication.class,args);
}
}
- 包结构
(2) 开通阿里云短信服务
阿里云短信服务开通的步骤可以看我的另一篇博客:https://blog.csdn.net/aetawt/article/details/127014869
(3) 后端整合阿里云短信服务
阿里云云自动生成SDK :https://next.api.aliyun.com/api/Dysmsapi/2017-05-25/SendSms?lang=JAVA¶ms={}
- msm 模块增加依赖
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
</dependency>
</dependencies>
- 引入工具类,该工具类随机生成 4-6 位验证码
public class RandomUtil {
private static final Random random = new Random();
private static final DecimalFormat fourdf = new DecimalFormat("0000");
private static final DecimalFormat sixdf = new DecimalFormat("000000");
public static String getFourBitRandom() {
return fourdf.format(random.nextInt(10000));
}
public static String getSixBitRandom() {
return sixdf.format(random.nextInt(1000000));
}
/**
* 给定数组,抽取n个数据
* @param list
* @param n
* @return
*/
public static ArrayList getRandom(List list, int n) {
Random random = new Random();
HashMap<Object, Object> hashMap = new HashMap<Object, Object>();
// 生成随机数字并存入HashMap
for (int i = 0; i < list.size(); i++) {
int number = random.nextInt(100) + 1;
hashMap.put(number, i);
}
// 从HashMap导入数组
Object[] robjs = hashMap.values().toArray();
ArrayList r = new ArrayList();
// 遍历数组并打印数据
for (int i = 0; i < n; i++) {
r.add(list.get((int) robjs[i]));
System.out.print(list.get((int) robjs[i]) + "\t");
}
System.out.print("\n");
return r;
}
}
- service 层使用阿里云提供的 SDK 实现短信服务
接口:
boolean send(String code, String phone);
实现类:
@Override
public boolean send(String code, String phone) {
if(StringUtils.isEmpty(phone)) return false;
// 将 code 封装成 map
Map<String, String> params = new HashMap<>();
params.put("code",code);
DefaultProfile profile =
DefaultProfile.getProfile("default", "your AccessKey ID", "your AccessKey Secret");
IAcsClient client = new DefaultAcsClient(profile);
CommonRequest request = new CommonRequest();
//request.setProtocol(ProtocolType.HTTPS);
request.setMethod(MethodType.POST);
request.setDomain("dysmsapi.aliyuncs.com");
request.setVersion("2017-05-25");
request.setAction("SendSms");
request.putQueryParameter("PhoneNumbers", phone); // 手机号
request.putQueryParameter("SignName", "你的签名"); // 签名
request.putQueryParameter("TemplateCode", "模板CODE"); // 模板CODE
request.putQueryParameter("TemplateParam", JSONObject.toJSONString(params)); // 验证码map。转换成json
try {
CommonResponse response = client.getCommonResponse(request);
System.out.println(response.getData());
return response.getHttpResponse().isSuccess();
} catch (ServerException e) {
e.printStackTrace();
} catch (ClientException e) {
e.printStackTrace();
}
return false;
}
- controller 层,对验证码的时长进行控制,超过5分钟验证码无效,可以将验证码存入redis,设置过期时间
@CrossOrigin
@RequestMapping("/msmService/msm")
@RestController
public class MsmController {
@Autowired
private MsmService msmService;
@Autowired
private RedisTemplate redisTemplate;
/**
* @description 向手机发送短信验证码
* @date 2022/9/3 21:33
* @param phone
* @return com.atguigu.commonutils.R
*/
@ApiOperation("发送验证码")
@GetMapping("/sendCode/{phone}")
private R sendCode(@PathVariable String phone) {
// 从 redis 取出验证码
String code1 = (String) redisTemplate.opsForValue().get(phone);
if (!StringUtils.isEmpty(code1)) {
R.ok();
}
// 随机生成验证码
String code = RandomUtil.getSixBitRandom();
boolean result = msmService.send(code, phone);
if (result) {
// 将 code 存入redis 并设置过期时间为 5 分钟
redisTemplate.opsForValue().set(phone, code, 5, TimeUnit.MINUTES);
return R.ok();
} else {
return R.error().message("发送短信失败");
}
}
}
- 使用 Swagger 测试
4.用户登录/注册功能
(1) 环境搭建
- 导入 sql 文件
- 创建 service_ucenter 模块,并使用 MyBatisX 插件生成代码
- 启动类
@SpringBootApplication
@ComponentScan("com.atguigu")
@MapperScan("com.atguigu.ucenter.mapper")
public class UcenterApplication {
public static void main(String[] args) {
SpringApplication.run(UcenterApplication.class,args);
}
}
- application 配置文件
# 服务端口
server.port=8005
# 服务名
spring.application.name=service-ucenter
# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/guli?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=1234
spring.redis.host=192.168.200.132
spring.redis.port=6379
spring.redis.database= 0
spring.redis.timeout=1800000
spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-wait=-1
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-idle=5
spring.redis.lettuce.pool.min-idle=0
#最小空闲
#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
- 实体类增加 @TableField 注解,实现自动填充
(2) 登录 – 后端
- controller 层
根据 手机号 和密码 登录,登录成功返回一个 token 信息
@PostMapping("frontLogin")
@ApiOperation("前台登录")
private R loginUser(@RequestBody UcenterMember member) {
// 登录成功返回一个 token,使用 jwt 生成
String token = memberService.login(member);
return R.ok().data("token", token);
}
- service 层
接口:
String login(UcenterMember member);
实现类:
- 判断手机号是否正确
- 判断密码是否正确
- 判断用户是否是禁用状态
@Override
public String login(UcenterMember member) {
// 校验信息
String password = member.getPassword();
String phone = member.getMobile();
if (StringUtils.isEmpty(password) || StringUtils.isEmpty(phone)) {
throw new GuliException(20001, "手机号或密码不能为空");
}
// 1. 验证手机号是否正确
QueryWrapper<UcenterMember> wrapper = new QueryWrapper<>();
wrapper.eq("mobile", phone);
UcenterMember ucenterMember = baseMapper.selectOne(wrapper);
if (ucenterMember == null) {
throw new GuliException(20001, "改手机号未注册");
}
// 2. 验证密码是否正确
// 数据库中的密码是进行加密之后的.因此需要 MD5加密之后在进行比较
if (!MD5.encrypt(password).equals(ucenterMember.getPassword())) {
throw new GuliException(20001, "密码错误");
}
// 3. 验证是否被禁用
if (ucenterMember.getIsDisabled() == 1) {
throw new GuliException(20001, "该用户被禁用");
}
// 4. 根据id和昵称生成 token
return JwtUtils.getJwtToken(ucenterMember.getId(), ucenterMember.getNickname());
}
(3) 注册-- 后端
- 创建注册对象,保存注册的属性
@Data
@Api("注册对象")
public class RegisterVo {
@ApiModelProperty(value = "昵称")
private String nickname;
@ApiModelProperty(value = "手机号")
private String mobile;
@ApiModelProperty(value = "密码")
private String password;
@ApiModelProperty(value = "验证码")
private String code;
}
- controller 层
@PostMapping("frontRegister")
@ApiOperation("前台注册")
private R registerUser(@RequestBody RegisterVo registerVo) {
return memberService.register(registerVo);
}
- service 层
接口:
R register(RegisterVo registerVo);
实现类:
- 判断数据会否为空
- 判断验证码是否失效
- 判断注册的手机号是否重复
@Override
public R register(RegisterVo registerVo) {
// 获取数据
String phone = registerVo.getMobile();
String password = registerVo.getPassword();
String nickname = registerVo.getNickname();
String code = registerVo.getCode();
// 判断数据是否为空
if (StringUtils.isEmpty(password) || StringUtils.isEmpty(phone)
|| StringUtils.isEmpty(code) || StringUtils.isEmpty(nickname)) {
throw new GuliException(20001, "注册失败");
}
// 判断验证码是否失效
String redis_code = (String) redisTemplate.opsForValue().get(phone);
if (!code.equals(redis_code)) {
throw new GuliException(20001,"验证码失效");
}
// 判断手机号是否重复
QueryWrapper<UcenterMember> wrapper = new QueryWrapper<>();
wrapper.eq("mobile", phone);
if (baseMapper.selectCount(wrapper) > 0) {
throw new GuliException(20001,"手机号重复");
}
// 保存数据库
UcenterMember ucenterMember = new UcenterMember();
BeanUtils.copyProperties(registerVo,ucenterMember);
// 设置默认的一个头像
ucenterMember.setAvatar("https://tse4-mm.cn.bing.net/th/id/OIP-C.FWcXMS8gv70TsJkGIHMjjgHaHa?pid=ImgDet&rs=1");
// 密码需要进行加密
ucenterMember.setPassword(MD5.encrypt(registerVo.getPassword()));
return baseMapper.insert(ucenterMember) == 0 ? R.error().message("注册失败") : R.ok();
}
(4) 根据 token 获取用户信息
@GetMapping("getUserInfo")
@ApiOperation("根据token获取用户信息")
private R getUserInfoByToken(HttpServletRequest request) {
// 根据 request 获取用户 ID
String userID = JwtUtils.getMemberIdByJwtToken(request);
UcenterMember member = memberService.getById(userID);
return R.ok().data("userInfo",member);
}
(5) 前台登录、注册页面搭建
- 安装 element-ui 和 vue-qriously【微信支付生成二维码】和 npm install js-cookie【往cookie存token用】
npm install element-ui
npm install vue-qriously
npm install js-cookie
- 修改 配置文件 nuxt-swiper-plugin.js ,引入插件
import Vue from 'vue'
import VueAwesomeSwiper from 'vue-awesome-swiper/dist/ssr'
import VueQriously from 'vue-qriously'
import ElementUI from 'element-ui' //element-ui的全部组件
import 'element-ui/lib/theme-chalk/index.css'//element-ui的css
Vue.use(ElementUI) //使用elementUI
Vue.use(VueQriously)
Vue.use(VueAwesomeSwiper)
- 在 layouts 目录下创建 sign.vue
<template>
<div class="sign">
<!--标题-->
<div class="logo">
<img src="~/assets/img/logo.png" alt="logo">
</div>
<!--表单-->
<nuxt/>
</div>
</template>
- 在 /layouts/default.vue 中修改登录注册的路径
- 在 pages 目录下创建 login.vue, register.vue
- login.vue 页面
<template>
<div class="main">
<div class="title">
<a class="active" href="/login">登录</a>
<span>·</span>
<a href="/register">注册</a>
</div>
<div class="sign-up-container">
<el-form ref="userForm" :model="user">
<el-form-item class="input-prepend restyle" prop="mobile" :rules="[{ required: true, message: '请输入手机号码', trigger: 'blur' },{validator: checkPhone, trigger: 'blur'}]">
<div >
<el-input type="text" placeholder="手机号" v-model="user.mobile"/>
<i class="iconfont icon-phone" />
</div>
</el-form-item>
<el-form-item class="input-prepend" prop="password" :rules="[{ required: true, message: '请输入密码', trigger: 'blur' }]">
<div>
<el-input type="password" placeholder="密码" v-model="user.password"/>
<i class="iconfont icon-password"/>
</div>
</el-form-item>
<div class="btn">
<input type="button" class="sign-in-button" value="登录" @click="submitLogin()">
</div>
</el-form>
<!-- 更多登录方式 -->
<div class="more-sign">
<h6>社交帐号登录</h6>
<ul>
<li><a id="weixin" class="weixin" target="_blank" href="http://qy.free.idcfengye.com/api/ucenter/weixinLogin/login"><i class="iconfont icon-weixin"/></a></li>
<li><a id="qq" class="qq" target="_blank" href="#"><i class="iconfont icon-qq"/></a></li>
</ul>
</div>
</div>
</div>
</template>
<script>
import '~/assets/css/sign.css'
import '~/assets/css/iconfont.css'
import cookie from 'js-cookie'
export default {
layout: 'sign',
data () {
return {
user:{
mobile:'',
password:''
},
loginInfo:{}
}
},
methods: {
// 自定义手机号码校验规则
checkPhone(rule, value, callback) {
//debugger
if (!/^1[34578]\d{9}$/.test(value)) {
return callback(new Error("手机号码格式不正确"));
}
return callback();
},
},
}
</script>
<style>
.el-form-item__error{
z-index: 9999999;
}
</style>
- register.vue 页面
<template>
<div class="main">
<div class="title">
<a href="/login">登录</a>
<span>·</span>
<a class="active" href="/register">注册</a>
</div>
<div class="sign-up-container">
<el-form ref="userForm" :model="params">
<el-form-item class="input-prepend restyle" prop="nickname" :rules="[{ required: true, message: '请输入你的昵称', trigger: 'blur' }]">
<div>
<el-input type="text" placeholder="你的昵称" v-model="params.nickname"/>
<i class="iconfont icon-user"/>
</div>
</el-form-item>
<el-form-item class="input-prepend restyle no-radius" prop="mobile" :rules="[{ required: true, message: '请输入手机号码', trigger: 'blur' },{validator: checkPhone, trigger: 'blur'}]">
<div>
<el-input type="text" placeholder="手机号" v-model="params.mobile"/>
<i class="iconfont icon-phone"/>
</div>
</el-form-item>
<el-form-item class="input-prepend restyle no-radius" prop="code" :rules="[{ required: true, message: '请输入验证码', trigger: 'blur' }]">
<div style="width: 100%;display: block;float: left;position: relative">
<el-input type="text" placeholder="验证码" v-model="params.code"/>
<i class="iconfont icon-phone"/>
</div>
<div class="btn" style="position:absolute;right: 0;top: 6px;width: 40%;">
<a href="javascript:" type="button" @click="getCodeFun()" :value="codeTest" style="border: none;background-color: none">{{codeTest}}</a>
</div>
</el-form-item>
<el-form-item class="input-prepend" prop="password" :rules="[{ required: true, message: '请输入密码', trigger: 'blur' }]">
<div>
<el-input type="password" placeholder="设置密码" v-model="params.password"/>
<i class="iconfont icon-password"/>
</div>
</el-form-item>
<div class="btn">
<input type="button" class="sign-up-button" value="注册" @click="submitRegister()">
</div>
<p class="sign-up-msg">
点击 “注册” 即表示您同意并愿意遵守简书
<br>
<a target="_blank" href="http://www.jianshu.com/p/c44d171298ce">用户协议</a>
和
<a target="_blank" href="http://www.jianshu.com/p/2ov8x3">隐私政策</a> 。
</p>
</el-form>
<!-- 更多注册方式 -->
<div class="more-sign">
<h6>社交帐号直接注册</h6>
<ul>
<li><a id="weixin" class="weixin" target="_blank" href="http://huaan.free.idcfengye.com/api/ucenter/wx/login"><i
class="iconfont icon-weixin"/></a></li>
<li><a id="qq" class="qq" target="_blank" href="#"><i class="iconfont icon-qq"/></a></li>
</ul>
</div>
</div>
</div>
</template>
<script>
import '~/assets/css/sign.css'
import '~/assets/css/iconfont.css'
export default {
layout: 'sign',
data() {
return {
params: {
mobile: '',
code: '',
nickname: '',
password: ''
},
sending: true, //是否发送验证码
second: 60, //倒计时间
codeTest: '获取验证码'
}
},
created() {
},
methods: {
// 自定义手机号码校验规则
checkPhone(rule, value, callback) {
//debugger
if (!/^1[34578]\d{9}$/.test(value)) {
return callback(new Error("手机号码格式不正确"));
}
return callback();
},
}
}
</script>
- :rules 是框架帮我们做的校验规则,
- required: 该输入框必须要填写内容
- message: 不符合规则显示的信息
- tigger : 触发机制,也就是什么时候出发
- validator: 自己定义规则
页面效果:
(6) 注册 --前端
- 在 api 目录下创建 register.js 文件,实现对后端端口的调用
import request from '@/utils/request'
export default {
// 1.发送验证码
sendCode(mobile) {
return request({
url: `/msmService/msm/sendCode/` + mobile,
method: 'get'
})
},
// 2.注册
register(registerVo) {
return request({
url: `ucenterService/ucenter/frontRegister`,
method: 'post',
data: registerVo
})
},
}
- register.vue 页面 引入 register.js 文件,调用 api 实现功能
import registerApi from "~/api/register";
- 注册的方法
- 发送验证码
- 发送验证码时,判断是否输入了手机号
- 验证码倒计时,在倒计时内不允许再次发送验证码
- 自定义手机号的校验规则
// 注册
submitRegister() {
registerApi.register(this.params).
then((response) => {
//提示注册成功
this.$message({
type: "success",
message: "注册成功",
});
// 跳转到登录界面
this.$router.push({ path: "/login" });
});
},
// 发送验证码
getCodeFun() {
// 判断有无手机号
if (!this.params.mobile) {
this.$message({
type: "warning",
message: "请输入手机号",
});
} else {
registerApi.sendCode(this.params.mobile).then((response) => {
this.sending = false;
this.timeDown();
});
}
},
// 发送验证码倒计时
timeDown() {
let result = setInterval(() => {
--this.second;
this.codeTest = this.second;
if (this.second < 1) {
clearInterval(result);
this.sending = true;
//this.disabled = false;
this.second = 60;
this.codeTest = "获取验证码";
}
}, 1000);
},
// 自定义手机号校验规则
checkPhone(rule, value, callback) {
//debugger
if (!/^1[34578]\d{9}$/.test(value)) {
return callback(new Error("手机号码格式不正确"));
}
return callback();
},
Nginx 增加请求转发:
(7) 登录 – 前端
- 点击 登录 调用 后端接口 login 方法,返回 token 字符串
- 将 token 字符串保存到 cookie 中
- 创建拦截器拦截请求,判断 cookie 是否有 token,如果有放到 请求头 中去
- 根据 token 获取用户信息,并将用户信息保存 cookie 中
- 首页显示用户信息
注意: 拦截器是拦截所有请求,但并不是阻止请求,他只是 判断 token,并放入 header 中去。
- api 目录下创建 login.js ,定义接口的 api
import request from '@/utils/request'
export default {
// 1.登录
login(user) {
return request({
url: `ucenterService/ucenter/frontLogin/` ,
method: 'post',
data: user
})
},
// 2.根据token获取用户信息
getUser() {
return request({
url: `ucenterService/ucenter/getUserInfo/`,
method: 'get',
})
},
}
- login.vue 中引入 cookie.js 和 login.js
import cookie from "js-cookie";
import loginApi from "~/api/login.js";
- 实现登录方法
- 第一步:调用登录方法,返回 token 字符串
- 第二步:将 token 字符串存入 cookie 中
- 第三步:创建拦截器,将 token 放入 request 的 header 中
- 第四步:获取用户信息,由于 每次请求头中都包含 token信息,后端根据 header 中的 token 查询用户信息
- 第五步:将用户信息保存到 cookie中
// 登录
submitLogin() {
// 第一步: 调用接口login方法
loginApi.login(this.user).then((response) => {
// 第二步: 将 token 放入 cookie 中
// 第一个参数: cookie 的 key 值
// 第二个参数: cookie 的 value 值
// 第三个参数: cookie 的 作用范围
cookie.set("guli_token", response.data.data.token, { domain: 'localhost' })
// 第四步: 获取用户信息,并将用户信息保存到 cookie 中
loginApi.getUser().then(response => {
// 由于后端返回的是 JSON 对象,而 cookie 中只能存储 字符串,所以把 JSON 对象转换成 JSON 字符串
this.loginInfo = JSON.stringify(response.data.data.userInfo)
cookie.set("userInfo",this.loginInfo, { domain: 'localhost' })
})
// 跳转首页,使用 $router.push 也可以
window.location.href = '/'
});
},
注意: JSON 对象 和 JSON 字符串的区别
JSON 对象: {‘name’ : ‘张三’ , ‘age’ : 10}
JSON 字符串: " {‘name’ : ‘张三’ , ‘age’ : 10}"
- 实现第三步创建拦截器,拦截器是拦截所有请求,因此在 request.js 中定义最为合适,因为每个 api 都引入了 request.js
import axios from 'axios'
import cookie from "js-cookie";
// 创建axios实例
const service = axios.create({
baseURL: 'http://192.168.200.132:9003', // api的base_url
timeout: 20000 // 请求超时时间
})
// 第三步: http request 拦截器
service.interceptors.request.use(
config => {
//debugger
// 判断 cookie 中是否有 token
if (cookie.get('guli_token')) {
// 如果有 token 放入 header 中
config.headers['token'] = cookie.get('guli_token');
}
return config
},
err => {
return Promise.reject(err);
})
export default service
- 在 default.vue 页面显示用户信息
HTML模板: 替换掉之前的 ul 列表
<!-- / nav -->
<ul class="h-r-login">
<li v-if="!loginInfo.id" id="no-login">
<a href="/login" title="登录">
<em class="icon18 login-icon"> </em>
<span class="vam ml5">登录</span>
</a>
|
<a href="/register" title="注册">
<span class="vam ml5">注册</span>
</a>
</li>
<li v-if="loginInfo.id" id="is-login-one" class="mr10">
<a id="headerMsgCountId" href="#" title="消息">
<em class="icon18 news-icon"> </em>
</a>
<q class="red-point" style="display: none"> </q>
</li>
<li v-if="loginInfo.id" id="is-login-two" class="h-r-user">
<a href="/ucenter" title>
<img
:src="loginInfo.avatar"
width="30"
height="30"
class="vam picImg"
alt
/>
<span id="userName" class="vam disIb">{{
loginInfo.nickname
}}</span>
</a>
<a
href="javascript:void(0);"
title="退出"
@click="logout()"
class="ml5"
>退出</a
>
</li>
<!-- /未登录显示第1 li;登录后显示第2,3 li -->
</ul>
JS 代码:
<script>
import "~/assets/css/reset.css";
import "~/assets/css/theme.css";
import "~/assets/css/global.css";
import "~/assets/css/web.css";
import cookie from "js-cookie";
export default {
data() {
return {
token: "",
// 用户登录信息
loginInfo: {
id: "",
age: "",
avatar: "",
mobile: "",
nickname: "",
sex: "",
},
};
},
created() {
this.showInfo();
},
methods: {
showInfo() {
// 从 cookie 中取出数据
var userInfo = cookie.get("userInfo");
// 从 cookie 中取出的数据是 JSON格式 的字符串,需要转换成JSON对象
if(userInfo) {
this.loginInfo = JSON.parse(userInfo);
console.log("首页:" + userInfo)
}
},
},
};
</script>
再次提醒: cookie 中存储的是字符串,而 data 中定义的 loginInfo 是 对象,不要忘记转换~~~~
效果:
(8) 用户退出功能
用户信息保存到 cookie 中,只需要清空 cookie 即可
// 用户退出
logout() {
cookie.set("guli_token", '', { domain: 'localhost' })
cookie.set("userInfo",'', { domain: 'localhost' })
window.location.href = '/'
}
5.微信扫描登录
(1) OAuth2 介绍
OAuth2 的定义:
其实就是给予应用有限的权限,代替用户访问用户的数据
那么是如何进行授权的呢?
OAuth2授权的核心就是 颁发 token 和使用 token
在 客户应用/ 第三方应用 向 授权服务器获取认证和授权后, 授权服务器为 客户应用/第三方 应用 颁发 token, 客户应用/ 第三方应用 拿着 token 去访问接口或者资源。
OAuth2 解决的问题:
- 开放系统间的授权
- 比如:下载一个游戏之后,需要授权手机的麦克风,相机等权限。这个就是授权。
- 分布式访问问题
- 类似单点登录
- 通过生成一定规则的字符串,保存到 cookie 中,其他服务判断 cookie 中是否有该字符串。
微信扫描登录就是使用的 OAuth2 授权…
(2) 获取微信扫描二维码
- 配置文件中增加
- appid、appsecret、redirect_url 是需要花钱 开通资质认证的。
- 使用以下提供好的就可以了。
# 微信开放平台 appid
wx.open.app_id=wxed9954c01bb89b47
# 微信开放平台 appsecret
wx.open.app_secret=a7482517235173ddb4083788de60b90e
# 微信开放平台 重定向url
wx.open.redirect_url=http://localhost:8160/api/ucenter/wx/callback
修改项目的端口号: 8160
必须是指定的端口 8160,不能换成其他的。
Nginx修改成 8160 端口 !!!!
- 创建常量类,获取配置文件中的值
@Component
public class WXConstantPropertiesUtil implements InitializingBean {
@Value("${wx.open.app_id}")
private String appId;
@Value("${wx.open.app_secret}")
private String appSecret;
@Value("${wx.open.redirect_url}")
private String redirectUrl;
public static String WX_OPEN_APP_ID;
public static String WX_OPEN_APP_SECRET;
public static String WX_OPEN_REDIRECT_URL;
@Override
public void afterPropertiesSet() throws Exception {
WX_OPEN_APP_ID = appId;
WX_OPEN_APP_SECRET = appSecret;
WX_OPEN_REDIRECT_URL = redirectUrl;
}
}
- 创建 WxApiController 用于微信登录
- 注意: 该 controller 用 @Controller 注解,不要用 @RestController注解。因为要实现 redirect 重定向
- 注意: redirect 和 路径之间是不能有空格的!!!!
redirect: "url"
; 这个 : 和 url 之间有空格是错误的。
@RequestMapping("/api/ucenter/wx")
@CrossOrigin
@Controller
public class WxApiController {
@ApiOperation("获取二维码")
@GetMapping("login")
private String getCode() {
// 向 微信平台 提供的固定地址发送请求,获取微信登录二维码
String url = memberService.getQRCode();
return "redirect:" + url;
}
}
- service 层:
接口:
String getQRCode();
实现类:
-
官方获取微信二维码的文档:资源中心 - 微信开放平台 (qq.com)
-
只需要按照文档给出的 url 地址 拼接参数就能获取二维码
-
拼接参数可以使用 + 号 的方式拼接,也可以使用 %s 占位符传参。
@Override
public String getQRCode() {
// 微信开放平台授权baseUrl
// %s 相当于占位符,一个 %s 传一个参数
String baseUrl = "https://open.weixin.qq.com/connect/qrconnect" +
"?appid=%s" +
"&redirect_uri=%s" +
"&response_type=code" +
"&scope=snsapi_login" +
"&state=%s" +
"#wechat_redirect";
// 对重定向的地址进行编码
String redirect_url = WXConstantPropertiesUtil.WX_OPEN_REDIRECT_URL;
try {
redirect_url = URLEncoder.encode(redirect_url, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
// 对 %s 进行传参: 第一个参数是对哪个字符串中的 %s 传参。 其余参数都是传的参数。
String finalUrl = String.format(baseUrl, WXConstantPropertiesUtil.WX_OPEN_APP_ID, redirect_url, "guli");
// 返回最终的跳转链接
return finalUrl;
}
效果: 访问:http://localhost:8160/api/ucenter/wx/login
(3) 获取 扫描人 的信息
当扫描二维码后,会调用配置文件中的 redirect_url 地址:http://localhost:8160/api/ucenter/wx/callback
所以我们需要 将 controller 层的路径改成与之对应的,该地址不能修改,固定的格式。
修改路径后不要忘记修改 Nginx 配置文件。
官方提供的完整流程图:
步骤分析:
http://localhost:8160/api/ucenter/wx/callback?code=071bOp0w3n93aZ2Sy62w3ZLZNj1bOp0I&state=guli
在微信扫码之后,会将 code 和 state 拼接在地址栏上。获取到 code 和 state
- code: 临时票据,可以理解为 手机验证码
- state : 原样返回的值,防止 csrf 攻击
拿着 code 和 state 向 微信平台 提供的一个固定地址:https://api.weixin.qq.com/sns/oauth2/access_token 发送第一次请求, 返回的是含有 access_token , openId 的 JSON 串,我们可以使用Gson,fastjson 等工具 将 JSON 串转换成 Map 集合,实现 key-value存储
- access_token : 就是授权的令牌,访问凭证
- openId: 微信的唯一标识
{ "access_token":"60_PlvEMzUELqlFyB-YvvXqoI_aI0k0JEm6VQHqi_Itr5-WS5wZIlP1ACPIlA6OuGztSNiFj3NKMx8gvchgJIRtdfHS2sKH7XYDxQMo9-mUoHU", "expires_in":7200, "refresh_token":"60_zEfm04m8XDgpdN5CRyzd1UZq-L5b-6LHZsPKMWi2Voipin2Bgd8AKB9lHqf1AiGmIzmbpbnD-RrXdIZXwfnLEs70Vdcqb3tyTQDkQY_-mqk", "openid":"o3_SC58j2tHoysGc6s2g_ZuGAiY", "scope":"snsapi_login", "unionid":"oWgGz1FCS3Va4apsBnsuAOkLwWVw" }
- 拿着 access_token 和 openId 在 向 微信平台 提供的一个固定的地址 : https://api.weixin.qq.com/sns/userinfo 发送第二次请求,最终就可以获取到扫描人的信息,头像等…
{ "openid":"o3_SC58-j2tHoysGc6s2g_ZuGAiY", "nickname":"梦想", "sex":0, "language":"", "city":"", "province":"", "country":"", "headimgurl":"https:\/\/thirdwx.qlogo.cn\/mmopen\/vi_32\/PiajxSqBRaEJUk3EWxOSHob9SACMf1CsiaSL8gnnWQGmMsC4oVpSQTSaibvSaoBRyV1uqeQe9cjoyp6pkiaPlQ8xsg\/132", "privilege":[], "unionid":"oWgGz1FCS3Va4apsBnsuAOkLwWVw" }
- 获取到用户信息之后,需要在页面中进行显示,而之前的做法是将用户信息保存到 cookie 中,在首页 从 cookie 中取出数据
而现在也可以这么做,但这么做有一个弊端: cookie 不能跨域。
因此我我们可以将 用户信息 使用 jwt 保存在 token 中,拼接在地址栏上,在首页进行获取。
由于我们需要在 方法内部是实现 发送请求,因此需要使用 HttpClient 技术。
- 在 common 模块下引入依赖
<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> </dependency> <!--commons-io--> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> </dependency> <!--gson--> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> </dependency>
- 复制 HttpClient 工具包 – 文档资料里有
实现代码:
- controller 层
@ApiOperation("获取扫描人的信息")
@GetMapping("callback")
private String getInfo(String code, String state) {
// 获取用户信息,并保存到 token 中
String token = memberService.callback(code,state);
return "redirect:http://localhost:3000?token=" + token;
}
- service 层
接口:
String callback(String code, String state);
实现类:
/**
* @description 获取扫描二维码的用户信息,并保存到 token 中
* @date 2022/9/5 18:17
* @param code
* @param state
* @return java.lang.String
*/
@Override
public String callback(String code, String state) {
try {
// 第一次发送请求的地址
String getTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token" +
"?appid=%s" +
"&secret=%s" +
"&code=%s" +
"&grant_type=authorization_code";
// 传递参数
String finalTokenUrl = String.format(getTokenUrl,
WXConstantPropertiesUtil.WX_OPEN_APP_ID,
WXConstantPropertiesUtil.WX_OPEN_APP_SECRET,
code);
// 使用 HttpClient 第一次发送请求: 根据 code 和 state 获取 token 和 openId
// 最终得到一个获得 JSON格式的字符串,包含着 access_token openId
String accessToken = HttpClientUtils.get(finalTokenUrl);
Gson gson = new Gson();
// 将 JSON 串转换成 map 集合
HashMap tokenMap = gson.fromJson(accessToken, HashMap.class);
// 获取 access_token,openId
String access_token = (String) tokenMap.get("access_token");
String openid = (String) tokenMap.get("openid");
// 根据微信id,查询用户,如果能查出来说明已经注册过,查不出来进行注册
QueryWrapper<UcenterMember> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("openid", openid);
UcenterMember member = this.getOne(queryWrapper);
if (member == null) {
// 说明没有注册过,没有扫过码
// 第二次发送请求
String getUserInfoUrl = "https://api.weixin.qq.com/sns/userinfo" +
"?access_token=%s" +
"&openid=%s";
String finalUserInfoUrl = String.format(getUserInfoUrl,
access_token, openid);
// 根据 access_token, openid 访问微信的资源服务器,获取用户信息
String userInfo = HttpClientUtils.get(finalUserInfoUrl);
HashMap userInfoMap = gson.fromJson(userInfo, HashMap.class);
// 获取用户信息
String nickname = (String) userInfoMap.get("nickname");
String headimgurl = (String) userInfoMap.get("headimgurl");
member = new UcenterMember();
member.setNickname(nickname);
member.setOpenid(openid);
member.setAvatar(headimgurl);
// 注册
this.save(member);
} else {
// 如果 member不等于null 说明注册过,判断用户是否被禁用
if (member.getIsDisabled() == 1) {
throw new GuliException(20001, "用户被禁用");
}
}
// 使用 jwt 生成 token 返回
return JwtUtils.getJwtToken(member.getId(), member.getNickname());
} catch (Exception e) {
e.printStackTrace();
throw new GuliException(20001, "登陆失败");
}
}
(4) 前台首页显示用户信息
实现步骤:
- 从路径中获取 token
- 将 token 存到 cookie 中
- 因为有拦截器,会判断 cookie 中是否有 token,如果有就将 token 保存到 请求 的 header 中去,并且 每次 请求 都会带有 header
- 根据 header 中的 token 获取用户信息,保存到 cookie 中。
-
在渲染页面之前,获取到路径中的参数
-
注意: 路径参数 和 ? 参数获取的方式
-
http://localhost:3000/edit/1 # 这种是路径传参,使用 this.$route.params.参数名 获得
-
http://localhost:3000/?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJndWxpLXVzZXIiLCJpYXQiOjE2NjIzNzU2OTksImV4cCI6MTY2MjQ2MjA5OSwiaWQiOiIxNTY2NzMxNTU0NTE1NDI3MzMwIiwibmlja25hbWUiOiLmoqbmg7MifQ.C4N3XU3q2SpIdEGWrgzUlAs28z9YifVFEF8-BSj8PPQ # 这种是 ? 传参,使用 this.$route.query.参数名 获取
-
created 中获取参数:
// 获取到路径中的 token
this.token = this.$route.query.token;
if (this.token) {
// 如果能取得到,获取用户信息
this.wxLogin();
} else {
this.showInfo();
}
- 在 methods 中获取用户信息
// 微信登录
wxLogin() {
// 将 token 存到 cookie 中
cookie.set("guli_token", this.token, { domain: "localhost" });
// 根据 token 获取用户信息
// 由于有拦截器,如果 cookie 中有 token ,拦截器就会把 token放到 header中,每次请求都会携带
// 后端通过 请求的header获取用户信息.
loginApi.getUser().then((response) => {
this.loginInfo = response.data.data.userInfo;
cookie.set("userInfo", this.loginInfo, { domain: "localhost" });
});
},
九、讲师模块
1.名师列表
点击名师,显示 名师 列表,每页显示八条数据
(1) 后端
- 在 service_edu 模块中,创建 TeacherFrontController 层
@RestController
@RequestMapping("/eduservice/teacherFront")
@CrossOrigin
@ApiModel(value = "前台讲师模块" ,description = "前台讲师模块")
public class TeacherFrontController {
@Autowired
private EduTeacherService teacherService;
@ApiOperation("分页查询讲师列表")
@PostMapping("/pageTeacher/{current}/{limit}")
private R pageTeacher(@PathVariable long current, @PathVariable long limit) {
// 分页查询讲师列表,将所有数据封装成 map 集合
Map<String, Object> pageData = teacherService.pageTeacher(current,limit);
return R.ok().data(pageData);
}
}
- service 层
接口
Map<String, Object> pageTeacher(long current, long limit);
实现类:
@Override
public Map<String, Object> pageTeacher(long current, long limit) {
Page<EduTeacher> eduTeacherPage = new Page<>(current,limit);
QueryWrapper<EduTeacher> wrapper = new QueryWrapper<>();
// 根据 sort 排序
wrapper.orderByAsc("sort");
this.page(eduTeacherPage,wrapper);
long current1 = eduTeacherPage.getCurrent();
List<EduTeacher> records = eduTeacherPage.getRecords();
long total = eduTeacherPage.getTotal();
long size = eduTeacherPage.getSize();
long pages = eduTeacherPage.getPages();
boolean hasNext = eduTeacherPage.hasNext();
boolean hasPrevious = eduTeacherPage.hasPrevious();
Map<String, Object> map = new HashMap<String, Object>();
map.put("items", records);
map.put("current", current);
map.put("pages", pages);
map.put("size", size);
map.put("total", total);
map.put("hasNext", hasNext);
map.put("hasPrevious", hasPrevious);
return map;
}
(2) 前端
- 在 api 目录下创建 teacher.js ,定义 dpi
import request from '@/utils/request'
export default {
// 1.分页查询讲师列表
getTeacherList(page,limit) {
return request({
url: `/eduservice/teacherFront/pageTeacher/${page}/${limit}`,
method: 'post'
})
},
}
- Js 代码
<script>
import teacherApi from "~/api/teacher";
export default {
data() {
return {
data: [],
page: 1,
limit: 8,
};
},
created() {
this.teacherList();
},
methods: {
// 查询讲师列表
teacherList(page = 1) {
teacherApi.getTeacherList(page, this.limit).then((response) => {
// 获取map集合
this.data = response.data.data;
});
},
// 跳转页码
gotoPage(page){
teacherApi.getTeacherList(page, this.limit).then((response) => {
// 获取map集合
this.data = response.data.data;
});
}
},
};
</script>
- 在无数据的时候,增加 v-if 指令 判断
、
- 删除多余的 li,使用 v-for 遍历讲师
<article class="i-teacher-list" v-if="data.total > 0">
<ul class="of">
<li v-for="teacher in data.items" :key="teacher.id">
<section class="i-teach-wrap">
<div class="i-teach-pic">
<a href="/teacher/1" :title="teacher.name" target="_blank">
<img :src="teacher.avatar" :alt="teacher.name" />
</a>
</div>
<div class="mt10 hLh30 txtOf tac">
<a
href="/teacher/1"
:title="teacher.name"
target="_blank"
class="fsize18 c-666"
>{{ teacher.name }}</a
>
</div>
<div class="hLh30 txtOf tac">
<span class="fsize14 c-999">{{ teacher.intro }}</span>
</div>
<div class="mt15 i-q-txt">
<p class="c-999 f-fA">{{ teacher.career }}</p>
</div>
</section>
</li>
</ul>
<div class="clear"></div>
</article>
- 分页条
<!-- 公共分页 开始 -->
<div>
<div class="paging">
<!-- undisable这个class是否存在,取决于数据属性hasPrevious -->
<a
:class="{ undisable: !data.hasPrevious }"
href="#"
title="首页"
@click.prevent="gotoPage(1)"
v-if="data.current != 1"
>首页</a
>
<a
:class="{ undisable: !data.hasPrevious }"
href="#"
title="前一页"
@click.prevent="gotoPage(data.current - 1)"
v-if="data.current != 1"
><</a
>
<a
v-for="page in data.pages"
:key="page"
:class="{
current: data.current == page,
undisable: data.current == page,
}"
:title="'第' + page + '页'"
href="#"
@click.prevent="gotoPage(page)"
>{{ page }}</a
>
<a
:class="{ undisable: !data.hasNext }"
href="#"
title="后一页"
@click.prevent="gotoPage(data.current + 1)"
v-if="data.current != data.pages"
>></a
>
<a
:class="{ undisable: !data.hasNext }"
href="#"
title="末页"
@click.prevent="gotoPage(data.pages)"
v-if="data.current != data.pages"
>末页</a
>
<div class="clear" />
</div>
</div>
<!-- 公共分页 结束 -->
2.名师详情
修改 讲师详情 的跳转链接:
(1) 后端
@ApiOperation("讲师详情")
@GetMapping("teacherInfoFront/{teacherId}")
private R getTeacherInfoFront(@PathVariable String teacherId){
// 1.根据教师id查询教师
EduTeacher eduTeacher = teacherService.getById(teacherId);
// 2.根据教师id查询课程
QueryWrapper<EduCourse> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("teacher_id",teacherId);
List<EduCourse> courseList = courseService.list(queryWrapper);
return R.ok().data("teacher",eduTeacher).data("courseList",courseList);
}
(1) 前端
- teacher.js 中定义 api
// 2.查询讲师详情
getTeacherInfo(teacherid) {
return request({
url: `/eduservice/teacherFront/teacherInfoFront/${teacherid}`,
method: 'get'
})
},
- _id.vue 页面,js 代码
<script>
import teacherApi from "~/api/teacher";
export default {
data() {
return {
teacher: '',
courseList:[],
teacherid: ''
}
},
created() {
// 从路径中获取参数
if (this.$route.params.id) {
this.teacherid = this.$route.params.id
}
this.teacherInfo()
},
methods: {
teacherInfo(){
teacherApi.getTeacherInfo(this.teacherid).then(response => {
this.teacher = response.data.data.teacher
this.courseList = response.data.data.courseList
})
}
},
};
</script>
- 在无数据的时候,增加 v-if 指令 判断,课程数组长度等于0说明没有课程信息,
- 教师详情
<div class="t-infor-wrap">
<!-- 讲师基本信息 -->
<section class="fl t-infor-box c-desc-content">
<div class="mt20 ml20">
<section class="t-infor-pic">
<img :src="teacher.avatar" />
</section>
<h3 class="hLh30">
<span class="fsize24 c-333">{{teacher.name}} {{ teacher.level===1?'高级讲师':'首席讲师' }}</span>
</h3>
<section class="mt10">
<span class="t-tag-bg">{{ teacher.intro }}</span>
</section>
<section class="t-infor-txt">
<p class="mt20">
{{teacher.career}}
</p>
</section>
<div class="clear"></div>
</div>
</section>
<div class="clear"></div>
</div>
- 主讲课程信息
<!-- /课程列表 开始-->
<article class="comm-course-list">
<ul class="of">
<li v-for="course in courseList" :key="course.id">
<div class="cc-l-wrap">
<section class="course-img">
<img :src="course.cover" class="img-responsive" />
<div class="cc-mask">
<a
:href="'/course/' + course.id"
title="开始学习"
target="_blank"
class="comm-btn c-btn-1"
>开始学习</a
>
</div>
</section>
<h3 class="hLh30 txtOf mt10">
<a
:href="'/course/' + course.id"
:title="course.title"
target="_blank"
class="course-title fsize18 c-333"
>{{ course.title }}</a
>
</h3>
</div>
</li>
</ul>
<div class="clear"></div>
</article>
<!-- /课程列表 结束-->
十、 课程模块
1.课程列表
(1) 后端
根据以上的条件查询课程的信息,并且有选择的进行排序
并且查询出来的课程应该是已经发布的课程
status 为 Normal 的课程。
- 将以上条件封装成对象
@Data
@ApiModel(description = "课程条件封装对象")
public class CourseFrontVo {
@ApiModelProperty(value = "课程名称")
private String title;
@ApiModelProperty(value = "讲师id")
private String teacherId;
@ApiModelProperty(value = "一级类别id")
private String subjectParentId;
@ApiModelProperty(value = "二级类别id")
private String subjectId;
@ApiModelProperty(value = "销量排序")
private String buyCountSort;
@ApiModelProperty(value = "最新时间排序")
private String gmtCreateSort;
@ApiModelProperty(value = "价格排序")
private String priceSort;
}
- controller 层
@RestController
@RequestMapping("/eduservice/courseFront")
@CrossOrigin
@ApiModel(value = "前台课程模块" ,description = "前台课程模块")
public class CourseFrontController {
@Autowired
private EduCourseService courseService;
@ApiOperation("条件分页查询课程列表")
@PostMapping("getCourseFrontList/{page}/{limit}")
private R getCourseFrontList(@PathVariable long page,
@PathVariable long limit,
@RequestBody(required = false) CourseFrontVo courseFrontVo){
Map<String,Object> map = courseService.getCourseFrontList(page,limit,courseFrontVo);
return R.ok().data(map);
}
}
- service 层
接口:
Map<String, Object> getCourseFrontList(long page, long limit, CourseFrontVo courseFrontVo);
实现类:
条件使用 condition 条件组装,比 if–else 真的省事。。
QueryWrapper 每组条件都有一个 condition 参数,该参数 是 boolean类型,如果为 true,则会组装后边的 条件,为 false,则不组装后面的 条件
@Override
public Map<String, Object> getCourseFrontList(long current, long limit, CourseFrontVo courseFrontVo) {
Page<EduCourse> page = new Page<>(current,limit);
QueryWrapper<EduCourse> queryWrapper = new QueryWrapper<>();
// 组装条件查询
queryWrapper.eq(!StringUtils.isEmpty(courseFrontVo.getSubjectParentId()) ,"subject_parent_id",courseFrontVo.getSubjectParentId())
.eq( !StringUtils.isEmpty(courseFrontVo.getSubjectId()), "subject_id",courseFrontVo.getSubjectId())
.orderByDesc(!StringUtils.isEmpty(courseFrontVo.getBuyCountSort()),"buy_count")
.orderByDesc( !StringUtils.isEmpty(courseFrontVo.getPriceSort()),"price")
.orderByDesc( !StringUtils.isEmpty(courseFrontVo.getGmtCreateSort()),"gmt_create")
// 查询已发布的课程
.eq("status","Normal");
this.page(page,queryWrapper);
List<EduCourse> records = page.getRecords();
long pageCurrent = page.getCurrent();
long pages = page.getPages();
long size = page.getSize();
long total = page.getTotal();
boolean hasNext = page.hasNext();
boolean hasPrevious = page.hasPrevious();
Map<String, Object> map = new HashMap<String, Object>();
map.put("items", records);
map.put("current", pageCurrent);
map.put("pages", pages);
map.put("size", size);
map.put("total", total);
map.put("hasNext", hasNext);
map.put("hasPrevious", hasPrevious);
return map;
}
(2) 前端
前端需要做的步骤:
- 查询出课程信息在页面中显示,带分页效果
- 查询出所有的 分类在页面中显示
- 点击 页码 进行跳转
- 点击 一级分类 显示对应的二级分类 以及 查询出对应的 课程信息
- 根据不同的条件进行排序
- 在 course.js 中定义 api
查询所有分类的方法,已经在 EduSubjectController 中写过了。
// 2.条件分页查询课程信息
getCourseFrontList(page, limit, searchObj) {
return request({
url: `/eduservice/courseFront/getCourseFrontList/${page}/${limit}`,
method: 'post',
data: searchObj
})
},
// 3.查询所有分类
getAllSubject() {
return request({
url: `/eduservice/subject/getAllSubject/`,
method: 'get',
})
}
并在/pages/course/index.vue 中引入 js
import courseApi from "~/api/course";
- 在
/pages/course/index.vue
页面中,data 中定义需要的数据
data() {
return {
page: 1,
limit: 8,
data: {}, // 保存课程数据
subjectNestedList: [], // 一级分类列表
subSubjectList: [], // 二级分类列表
searchObj: {}, // 查询表单对象
// 以下数据是为了点击 条件 显示样式用的
oneIndex: -1,
twoIndex: -1,
buyCountSort: "",
gmtCreateSort: "",
priceSort: "",
};
},
-
第一个功能:显示课程信息列表,并带分页。
-
methods 中定义方法,调用 api
-
// 1.查询课程信息 selectTeacherList() { courseApi .getCourseFrontList(this.page, this.limit, this.searchObj) .then((response) => { this.data = response.data.data; }); },
并且在 created 中进行调用
created() { // 查询课程信息 this.selectTeacherList(); },
-
删除多余的 li,使用 v-for 循环遍历课程信息
-
<article class="comm-course-list"> <ul class="of" id="bna"> <!-- 遍历课程信息 --> <li v-for="(course, index) in data.items" :key="index"> <div class="cc-l-wrap"> <section class="course-img"> <img :src="course.cover" class="img-responsive" :alt="course.title" /> <div class="cc-mask"> <a :href="'/course/' + course.id" title="开始学习" class="comm-btn c-btn-1" >开始学习</a > </div> </section> <h3 class="hLh30 txtOf mt10"> <a :href="'/course/' + course.id" title="听力口语" class="course-title fsize18 c-333" >{{ course.title }}</a > </h3> <section class="mt10 hLh20 of"> <span class="fr jgTag bg-green" v-if="Number(course.price) == 0" > <i class="c-fff fsize12 f-fA">免费</i> </span> <span class="fr jgTag bg-green" v-else> <i class="c-fff fsize12 f-fA">{{ course.price }}</i> </span> <span class="fl jgAttr c-ccc f-fA"> <i class="c-999 f-fA">{{ course.buyCount }}</i> | <i class="c-999 f-fA">{{ course.viewCount }}</i> </span> </section> </div> </li> </ul> <div class="clear"></div> </article>
-
分页条模板
-
<!-- 公共分页 开始 --> <div> <div class="paging"> <!-- undisable这个class是否存在,取决于数据属性hasPrevious --> <a :class="{ undisable: !data.hasPrevious }" href="#" title="首页" @click.prevent="gotoPage(1)" v-if="data.current != 1" >首页</a > <a :class="{ undisable: !data.hasPrevious }" href="#" title="前一页" @click.prevent="gotoPage(data.current - 1)" v-if="data.current != 1" ><</a > <a v-for="page in data.pages" :key="page" :class="{ current: data.current == page, undisable: data.current == page, }" :title="'第' + page + '页'" href="#" @click.prevent="gotoPage(page)" >{{ page }}</a > <a :class="{ undisable: !data.hasNext }" href="#" title="后一页" @click.prevent="gotoPage(data.current + 1)" v-if="data.current != data.pages" >></a > <a :class="{ undisable: !data.hasNext }" href="#" title="末页" @click.prevent="gotoPage(data.pages)" v-if="data.current != data.pages" >末页</a > <div class="clear" /> </div> </div> <!-- 公共分页 结束 -->
-
定义 gotoPage 方法,实现页码的跳转
-
// 3. 点击页码进行跳转 gotoPage(page) { courseApi .getCourseFrontList(page, this.limit, this.searchObj) .then((response) => { this.data = response.data.data; }); },
-
-
第二个功能: 显示所有的一级分类
-
methods 中调用 api
- 注意:查询出来的分类是所有的一级分类,而每个一级分类里有个 children 集合,保存二级分类
// 2.获取所有的分类 selectAllSubject() { courseApi.getAllSubject().then((response) => { this.subjectNestedList = response.data.data.list; }); },
-
页面中使用 v-for 循环遍历所有的一级分类
-
:class="{ active: oneIndex == index }"
: 如果自定义的索引neIndex 等于 遍历时的索引,说明用户点击了该一级分类,就带上选中的样式 -
@click="searchOneSubjectList(subject.id, index)"
: 该方法是点击一级分类,查询对应的课程,并且找出对应的二级分类 -
<!-- 遍历所有的一级分类 --> <li v-for="(subject, index) in subjectNestedList" :key="index" :class="{ active: oneIndex == index }" > <a :title="subject.title" href="#" @click="searchOneSubjectList(subject.id, index)" >{{ subject.title }}</a > </li>
-
-
第三个功能:获取所有的二级分类在页面显示,并且根据一级分类查询课程信息
-
点击
一级分类
显示对应的二级分类
,做成联动效果 -
再点击 一级分类时,将 一级分类的 Id 传 searchOneSubjectList 方法里。
-
遍历所有的一级分类 ,将 id 和点击的 一级分类 id 作比较,相等就把 一级分类的 children 集合赋值到二级分类集合
-
// 4. 点击 一级分类,显示对应的二级分类,并进行查询 searchOneSubjectList(subjectParentId, index) { // 点击一级分类,显示选中效果 this.oneIndex = index; this.twoIndex = -1; // 清空二级分类Id,只对一级分类查询 this.searchObj.subjectId = ""; this.subSubjectList = []; // 将一级分类id赋值给条件查询对象 this.searchObj.subjectParentId = subjectParentId; // 根据一级分类查询 this.gotoPage(1, this.limit, this.searchObj); // 遍历所有的一级分类,根据一级分类id找出对应的二级分类 this.subjectNestedList.forEach((element) => { // 如果点击的一级分类id与遍历的一级分类id相等,就将一级分类里的二级分类赋值给 subSubjectList if (subjectParentId == element.id) { this.subSubjectList = element.children; } }); },
-
使用 v-for 遍历二级分类
-
:class="{ active: twoIndex == index }"
: 如果自定义的索引neIndex 等于 遍历时的索引,说明用户点击了该一级分类,就带上选中的样式 -
@click="searchTwoSubjectList(subject.id, index)"
: 该方法是点击二级分类,查询对应的课程 -
<ul class="clearfix"> <!-- 遍历所有的二级分类 --> <li v-for="(subject, index) in subSubjectList" :key="index" :class="{ active: twoIndex == index }" > <a :title="subject.title" href="#" @click="searchTwoSubjectList(subject.id, index)" >{{ subject.title }}</a > </li> </ul>
-
-
第四个功能:点击二级分类,查询对应课程信息
-
// 5.点击二级分类,进行查询 searchTwoSubjectList(subjectId, index) { // 点击二级分类,显示选中效果 this.twoIndex = index; // 将二级分类id赋值给条件查询对象 this.searchObj.subjectId = subjectId; // 根据二级分类查询 this.gotoPage(1, this.limit, this.searchObj); },
-
-
第五个功能:根据不同的条件进行排序
-
点击哪个条件,就让哪个条件不为空 ,并封装到 searchObj 条件查询对象中,进行查询~
-
:class="{ 'current bg-orange': buyCountSort != '' }"
: buyCountSort 不为空,就为该按钮增加一个样式。 -
<!-- 根据不同选项排序 --> <section class="fl"> <ol class="js-tap clearfix"> <li :class="{ 'current bg-orange': buyCountSort != '' }"> <a title="销量" href="javascript:void(0);" @click="searchBuyCount()" >销量 <span :class="{ hide: buyCountSort == '' }">↓</span> </a> </li> <li :class="{ 'current bg-orange': gmtCreateSort != '' }"> <a title="最新" href="javascript:void(0);" @click="searchGmtCreate()" >最新 <span :class="{ hide: gmtCreateSort == '' }">↓</span> </a> </li> <li :class="{ 'current bg-orange': priceSort != '' }"> <a title="价格" href="javascript:void(0);" @click="searchPrice()" >价格 <span :class="{ hide: priceSort == '' }">↓</span> </a> </li> </ol> </section>
-
Js 代码
// 6.根据销量进行排序 searchBuyCount() { // 只要有值就可以,是什么无所谓 this.buyCountSort = "1"; this.priceSort = ""; this.gmtCreateSort = ""; // 将条件赋值给 条件对象 this.searchObj.buyCountSort = this.buyCountSort; this.searchObj.gmtCreateSort = this.gmtCreateSort; this.searchObj.priceSort = this.priceSort; // 查询 this.gotoPage(this.page, this.limit, this.searchObj); }, // 7.更新时间查询 searchGmtCreate() { this.buyCountSort = ""; this.gmtCreateSort = "1"; this.priceSort = ""; this.searchObj.buyCountSort = this.buyCountSort; this.searchObj.gmtCreateSort = this.gmtCreateSort; this.searchObj.priceSort = this.priceSort; this.gotoPage(this.page, this.limit, this.searchObj); }, // 8.价格查询 searchPrice() { this.buyCountSort = ""; this.gmtCreateSort = ""; this.priceSort = "1"; this.searchObj.buyCountSort = this.buyCountSort; this.searchObj.gmtCreateSort = this.gmtCreateSort; this.searchObj.priceSort = this.priceSort; this.gotoPage(this.page, this.limit, this.searchObj); },
-
-
第六个功能: 点击
全部
显示所有的课程,并带上选中的样式,把其他选项样式去掉-
增加点击事件,并在 data 中定义 wholeIndex。
-
:class="{ active: wholeIndex != 0 }"
: 只要 wholeIndex 不等 0 就显示样式 -
<li> <a title="全部" href="#" @click="selectAll()" :class="{ active: wholeIndex != 0 }">全部</a> </li>
wholeIndex: 1
-
Js 代码中只需要将其他条件 置空 然后进行查询即可。
-
// 9. 查询全部,并清空所有条件 selectAll() { // '全部' 样式 this.wholeIndex = 1 this.buyCountSort = ""; this.gmtCreateSort = ""; this.priceSort = ""; this.oneIndex = -1 this.twoIndex = -1 this.subSubjectList = [] this.searchObj = {} this.gotoPage(this.page, this.limit, this.searchObj); }
-
在 searchPrice、searchGmtCreate、searchBuyCount、searchTwoSubjectList、searchOneSubjectList 方法中将 wholeIndex 置 0
-
2.课程详情
(1) 后端
、
课程详情包含俩部分:
- 第一部分,是关于课程信息,需要查询多张表。课程表、讲师表、课程描述表…使用 SQL 的多表联查实现
- 第二部分:是课程的章节信息,根据 courseId 查询即可,写过该方法。。。
- 对于课程详情的信息,使用一个 Vo 类,将所有信息的属性封装起来,通过 sql 联查语句查询
@Data
public class CourseWebVo {
private static final long serialVersionUID = 1L;
private String id;
@ApiModelProperty(value = "课程标题")
private String title;
@ApiModelProperty(value = "课程销售价格,设置为0则可免费观看")
private BigDecimal price;
@ApiModelProperty(value = "总课时")
private Integer lessonNum;
@ApiModelProperty(value = "课程封面图片路径")
private String cover;
@ApiModelProperty(value = "销售数量")
private Long buyCount;
@ApiModelProperty(value = "浏览数量")
private Long viewCount;
@ApiModelProperty(value = "课程简介")
private String description;
@ApiModelProperty(value = "讲师ID")
private String teacherId;
@ApiModelProperty(value = "讲师姓名")
private String teacherName;
@ApiModelProperty(value = "讲师资历,一句话说明讲师")
private String intro;
@ApiModelProperty(value = "讲师头像")
private String avatar;
@ApiModelProperty(value = "课程类别ID")
private String subjectLevelOneId;
@ApiModelProperty(value = "一级分类")
private String subjectLevelOne;
@ApiModelProperty(value = "课程类别ID")
private String subjectLevelTwoId;
@ApiModelProperty(value = "二级分类")
private String subjectLevelTwo;
}
- EduCourseMapper
/**
* @description 查询课程详情
* @date 2022/9/6 21:59
* @param courseId
* @return java.util.List<com.atguigu.demo.entity.frontVo.CourseWebVo>
*/
CourseWebVo getCourseFrontInfo(String courseId);
- EduCourseMapper 文件,定义 sql 语句联查语句
<!--查询课程详情-->
<select id="getCourseFrontInfo" resultType="com.atguigu.demo.entity.frontVo.CourseWebVo">
SELECT c.id,
c.title,
c.cover,
CONVERT(c.price, DECIMAL (8, 2)) AS price,
c.lesson_num AS lessonNum,
c.cover,
c.buy_count AS buyCount,
c.view_count AS viewCount,
cd.description,
t.id AS teacherId,
t.name AS teacherName,
t.intro,
t.avatar,
s1.id AS subjectLevelOneId,
s1.title AS subjectLevelOne,
s2.id AS subjectLevelTwoId,
s2.title AS subjectLevelTwo
FROM edu_course c
LEFT JOIN edu_course_description cd ON c.id = cd.id
LEFT JOIN edu_teacher t ON c.teacher_id = t.id
LEFT JOIN edu_subject s1 ON c.subject_parent_id = s1.id
LEFT JOIN edu_subject s2 ON c.subject_id = s2.id
WHERE c.id = #{id}
</select>
- service 层
接口:
/**
* @description 查询课程详情
* @date 2022/9/6 21:59
* @param courseId
* @return java.util.List<com.atguigu.demo.entity.frontVo.CourseWebVo>
*/
CourseWebVo getCourseFrontInfo(String courseId);
实现类:
@Override
public CourseWebVo getCourseFrontInfo(String courseId) {
return courseMapper.getCourseFrontInfo(courseId);
}
- EduCourseFrontController
/**
* @description 根据 courseId 查询课程详情
* @date 2022/9/6 22:11
* @param courseId
* @return com.atguigu.commonutils.R
*/
@GetMapping("getCourseFrontInfo/{courseId}")
@ApiOperation("查询课程详情")
private R getCourseFrontInfo(@PathVariable String courseId) {
// 1.查询课程基本信息
CourseWebVo courseInfo = courseService.getCourseFrontInfo(courseId);
// 2.查询章节信息
List<ChapterVo> allChapterVideo = chapterService.getAllChapterVideo(courseId);
return R.ok().data("courseInfo", courseInfo).data("chapterVideoList", allChapterVideo);
}
(2) 前端
- 在 course.js 中定义 api
// 4.查询课程详情
getCourseDetailInfo(courseId) {
return request({
url: `/eduservice/courseFront/getCourseFrontInfo/` + courseId,
method: 'get',
})
}
- 页面中做调用
<script>
import courseApi from "~/api/course";
export default {
data() {
return {
courseInfo: {},
chapterVideoList: {},
courseId: "",
};
},
created() {
// 获取路径中的id
if (this.$route.params.id) {
this.courseId = this.$route.params.id;
}
this.getCourseInfo();
},
methods: {
// 查询课程详情
getCourseInfo() {
courseApi.getCourseDetailInfo(this.courseId).then((response) => {
this.courseInfo = response.data.data.courseInfo;
this.chapterVideoList = response.data.data.chapterVideoList;
});
},
},
};
</script>
- 课程详情页面显示
注意: 由于课程描述中增加了富文本编辑器,含有 html 标签,需要使用 v-html 标签解析。
<template>
<div id="aCoursesList" class="bg-fa of">
<!-- /课程详情 开始 -->
<section class="container">
<!-- 课程分类 -->
<section class="path-wrap txtOf hLh30">
<a href="#" title class="c-999 fsize14">首页</a>
\
<a href="#" title class="c-999 fsize14">
{{ courseInfo.subjectLevelOne }}</a>
\
<span class="c-333 fsize14">{{ courseInfo.subjectLevelTwo }}</span>
</section>
<!-- 课程基本信息 -->
<div>
<article class="c-v-pic-wrap" style="height: 357px">
<section class="p-h-video-box" id="videoPlay">
<img
:src="courseInfo.cover"
:alt="courseInfo.title"
class="dis c-v-pic"
/>
</section>
</article>
<aside class="c-attr-wrap">
<section class="ml20 mr15">
<h2 class="hLh30 txtOf mt15">
<span class="c-fff fsize24">{{ courseInfo.title }}</span>
</h2>
<section class="c-attr-jg" v-if="Number(courseInfo.price) == 0">
<b class="c-yellow" style="font-size: 24px">免费</b>
</section>
<section class="c-attr-jg" v-else>
<span class="c-fff">价格:</span>
<b class="c-yellow" style="font-size: 24px"
>¥{{ courseInfo.price }}</b
>
</section>
<section class="c-attr-mt c-attr-undis">
<span class="c-fff fsize14"
>主讲: {{ courseInfo.teacherName }} </span
>
</section>
<section class="c-attr-mt of">
<span class="ml10 vam">
<em class="icon18 scIcon"></em>
<a class="c-fff vam" title="收藏" href="#">收藏</a>
</span>
</section>
<section class="c-attr-mt">
<a href="#" title="立即观看" class="comm-btn c-btn-3">立即观看</a>
</section>
</section>
</aside>
<aside class="thr-attr-box">
<ol class="thr-attr-ol clearfix">
<li>
<p> </p>
<aside>
<span class="c-fff f-fM">购买数</span>
<br />
<h6 class="c-fff f-fM mt10">{{ courseInfo.buyCount }}</h6>
</aside>
</li>
<li>
<p> </p>
<aside>
<span class="c-fff f-fM">课时数</span>
<br />
<h6 class="c-fff f-fM mt10">{{ courseInfo.lessonNum }}</h6>
</aside>
</li>
<li>
<p> </p>
<aside>
<span class="c-fff f-fM">浏览数</span>
<br />
<h6 class="c-fff f-fM mt10">{{ courseInfo.viewCount }}</h6>
</aside>
</li>
</ol>
</aside>
<div class="clear"></div>
</div>
<!-- 课程介绍 -->
<div class="mt20 c-infor-box">
<article class="fl col-7">
<section class="mr30">
<div class="i-box">
<div>
<section id="c-i-tabTitle" class="c-infor-tabTitle c-tab-title">
<a name="c-i" class="current" title="课程详情">课程详情</a>
</section>
</div>
<article class="ml10 mr10 pt20">
<div>
<h6 class="c-i-content c-infor-title">
<span>课程介绍</span>
</h6>
<div class="course-txt-body-wrap">
<section class="course-txt-body">
<!-- v-html 将课程简介中的html标签解析 -->
<p v-html="courseInfo.description">
{{ courseInfo.description }}
</p>
</section>
</div>
</div>
<!-- 章节大纲 -->
<div class="mt50">
<h6 class="c-g-content c-infor-title">
<span>课程大纲</span>
</h6>
<section class="mt20">
<div class="lh-menu-wrap">
<menu id="lh-menu" class="lh-menu mt10 mr10">
<ul>
<!-- 文件目录 -->
<li
class="lh-menu-stair"
v-for="chapter in chapterVideoList"
:key="chapter.id"
>
<a
href="javascript: void(0)"
title="第一章"
class="current-1"
>
<em class="lh-menu-i-1 icon18 mr10"></em
>{{ chapter.title }}
</a>
<ol class="lh-menu-ol" style="display: block">
<li
class="lh-menu-second ml30"
v-for="video in chapter.children"
:key="video.id"
>
<a href="#" title>
<span v-if="Number(courseInfo.price) === 0" class="fr">
<i class="free-icon vam mr10">免费试听</i>
</span>
<em class="lh-menu-i-2 icon16 mr5"> </em
>{{ video.title }}
</a>
</li>
</ol>
</li>
</ul>
</menu>
</div>
</section>
</div>
<!-- 主讲讲师 -->
</article>
</div>
</section>
</article>
<aside class="fl col-3">
<div class="i-box">
<div>
<section class="c-infor-tabTitle c-tab-title">
<a title href="javascript:void(0)">主讲讲师</a>
</section>
<section class="stud-act-list">
<ul style="height: auto">
<li>
<div class="u-face">
<a href="#">
<img
:src="courseInfo.avatar"
width="50"
height="50"
alt
/>
</a>
</div>
<section class="hLh30 txtOf">
<a class="c-333 fsize16 fl" href="#">{{
courseInfo.teacherName
}}</a>
</section>
<section class="hLh20 txtOf">
<span class="c-999">{{ courseInfo.intro }}</span>
</section>
</li>
</ul>
</section>
</div>
</div>
</aside>
<div class="clear"></div>
</div>
</section>
<!-- /课程详情 结束 -->
</div>
</template>
更多推荐
所有评论(0)