项目分为三篇:
谷粒学苑项目前置知识
谷粒学苑项目前台界面: 由于字数限制,分为俩部分,此篇为第一部分,第二部分
谷粒学苑后台管理系统
额外增加的功能:
后台 课程 小节的 删改 操作
课程列表的 分页查询和 条件查询
前台 banner 图的自动播放
后台 banner 的增删改
后台 对 前台轮播图的图片数量做一个设置。比如设置 5 张图片轮播,设置 3张图片轮播
课程详情
全部
按钮的实现课程评论功能
资料链接:谷粒学苑
提取码:p6er
视频教程:尚硅谷-谷粒学苑
前端代码:前端代码
后端代码:后端代码
将下载好的模板,里面的 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 [email protected]
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'
]
}
<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>
<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>
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:[email protected]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>
<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
路径是固定地址,不发生变化
使用 标签实现跳转, 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">1i>/
<i class="c-666 f-fM">1i>
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">1a>
<a href="#" title>2a>
<a id="nextpage" href="#" title>>a>
<a href="#" title>末a>
<div class="clear">div>
div>
div>
section>
section>
div>
template>
<script>
export default {};
script>
路径是变化的,比如课程详情页面, 根据 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.00b>
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">150h6>
aside>
li>
<li>
<p> p>
<aside>
<span class="c-fff f-fM">课时数span>
<br>
<h6 class="c-fff f-fM mt10">20h6>
aside>
li>
<li>
<p> p>
<aside>
<span class="c-fff f-fM">浏览数span>
<br>
<h6 class="c-fff f-fM mt10">501h6>
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>
npm install 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 学习笔记: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
项目整合 Redis :
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
dependency>
@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;
}
}
# 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
@Cacheable
注解注意: key 的 命名,需要一个
双引号
和单引号
。
# 服务端口
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
实体类中增加 自动填充 注解:
@SpringBootApplication
@MapperScan("com.atguigu.cms.mapper")
@ComponentScan("com.atguigu")
public class CmsMainApplication {
public static void main(String[] args) {
SpringApplication.run(CmsMainApplication.class,args);
}
}
Nginx 配置文件增加请求转发路径:
需要做的功能:
- 后台对 banner 图的管理
- 显示所有banner图的列表
- 设置前台显示的 banner 图
- 增加 banner 图
- 删除 banner 图
- 前台显示 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());
}
}
后台管理 – 前端设计:
// 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' }
}
]
},
// 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',
})
},
}
<template>
<div class="app-container">
<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>
<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 : 开启图片预览功能,
参数是一个数组类型
<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
效果演示:
点击设置 弹出设置框:
设置框里面的 banner 图,单击有预览效果
里面的 banner 图是根据数据库关联的,勾选上哪个 banner 图,前台对应显示哪个 banner图。
思路分析:
- 首先查询数据库在对话框中显示所有的 banner 图,这其实很简单。
- 使用 js 再点击确定的时候,判断有哪些banner 选中了,并将 这些 banner 图 的 ID 保存到一个集合中。
- 将 ID 集合 传到后端,后端根据这个 ID 集合 查询数据库,保存到 redis 缓存中
- 前台界面先从 redis 取 banner,如果有就直接取,没有查询数据库 ,显示 banner
后台管理 – 后端设计:
接口:
在接口中增加一个方法,该方法是后台管理去调用,往 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
*/
@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;
}
}
@ApiOperation(value = "修改前台banner图")
@PostMapping("editFrontBannerCount")
public R editFrontBannerCount(@RequestBody List<String> bannerIds) {
bannerService.editFrontBannerCount(bannerIds);
return R.ok();
}
后端管理 – 前端设计:
editBannerCount(bannerIds) {
return request({
url: `cmsService/cmsAdmin/editFrontBannerCount/`,
method: 'post',
data: bannerIds
})
}
<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 是打开对话框执行的回调函数
// 设置轮播图
dialogFormVisible: false,
bannerIds: [], // 保存选中的图片的ID
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();
});
});
},
增加 banner 图需要做的事:
- 图片上传到 oss
- 图片的地址存到数据库
后端设计:
@ApiOperation(value = "新增Banner")
@PostMapping("saveBanner")
public R save(@RequestBody CrmBanner banner) {
bannerService.save(banner);
return R.ok();
}
前端设计:
// 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>
<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>
后端设计:
@ApiOperation(value = "删除Banner")
@DeleteMapping("removeBanner/{id}")
public R remove(@PathVariable String id) {
bannerService.removeById(id);
return R.ok();
}
前端设计:
// 4.删除 banner
deleteBanner(bannerId) {
return request({
url: `cmsService/cmsAdmin/removeBanner/` + bannerId,
method: 'delete',
})
},
removeDataById(bannerId) {
this.$confirm("此操作将永久删除banner, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
// 点击 确定 执行的方法
banner.deleteBanner(bannerId).then(response => {
// 删除成功的方法
this.$message({
type: "success",
message: "删除成功!",
});
// 删除后重新查询banner列表
this.getBannerList();
});
})
},
在 前台中显示的 banner 图,是根据 后台管理 设置而来的,如果没有设置,那就好说了,直接查询数据库显示所有的 banner,或者 显示几个都行,自己定。
判断后台是否设置了 banner 图,只需要从 redis 取值是否 为空,空肯定没有设置,不为空就取出 值。
前台显示 – 后端设计:
@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);
}
}
接口:
// 前台显示 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中一样,只不过有些目录需要自己创建
调用接口的方式 和后台管理系统中 一模一样
import request from '@/utils/request'
export default {
// 1.获取 banner
getList() {
return request({
url: `/cmsService/cmsFront/getAllBanner`,
method: 'get'
})
}
}
// 保存 banner 图像
bannerList: [],
methods: {
getBannerList() {
banner.getList().then((response) => {
// 这里和写后台管理不一样, 需要俩次 .data
// 因为后台管理中,帮我们封装了一次 .data
console.log(response.data.data.bannerList)
this.bannerList = response.data.data.bannerList
})
},
}
created() {
this.getBannerList()
},
<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>
src:图片地址,我是将图片上传到 oss 中了,然后将图片地址保存到数据库中
href : 点击图片跳转的路径
在主页上,显示 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);
}
}
import request from '@/utils/request'
export default {
// 1.获取 课程 和 教师 列表
getCourseTeacherList() {
return request({
url: `/eduservice/front/index`,
method: 'get'
})
}
}
(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 是按照一定规则 生成的字符串,而 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)
<dependencies>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
dependency>
dependencies>
对我们来说所需要修改的地方:
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");
}
}
# 服务端口
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);
}
}
阿里云短信服务开通的步骤可以看我的另一篇博客:https://blog.csdn.net/aetawt/article/details/127014869
阿里云云自动生成SDK :https://next.api.aliyun.com/api/Dysmsapi/2017-05-25/SendSms?lang=JAVA¶ms={}
<dependencies>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
dependency>
<dependency>
<groupId>com.aliyungroupId>
<artifactId>aliyun-java-sdk-coreartifactId>
dependency>
dependencies>
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;
}
}
接口:
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;
}
@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("发送短信失败");
}
}
}
@SpringBootApplication
@ComponentScan("com.atguigu")
@MapperScan("com.atguigu.ucenter.mapper")
public class UcenterApplication {
public static void main(String[] args) {
SpringApplication.run(UcenterApplication.class,args);
}
}
# 服务端口
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
根据 手机号 和密码 登录,登录成功返回一个 token 信息
@PostMapping("frontLogin")
@ApiOperation("前台登录")
private R loginUser(@RequestBody UcenterMember member) {
// 登录成功返回一个 token,使用 jwt 生成
String token = memberService.login(member);
return R.ok().data("token", token);
}
接口:
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());
}
@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;
}
@PostMapping("frontRegister")
@ApiOperation("前台注册")
private R registerUser(@RequestBody RegisterVo registerVo) {
return memberService.register(registerVo);
}
接口:
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();
}
@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);
}
npm install element-ui
npm install vue-qriously
npm install js-cookie
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)
<template>
<div class="sign">
<div class="logo">
<img src="~/assets/img/logo.png" alt="logo">
div>
<nuxt/>
div>
template>
<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>
<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: 自己定义规则
页面效果:
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
})
},
}
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 增加请求转发:
- 点击 登录 调用 后端接口 login 方法,返回 token 字符串
- 将 token 字符串保存到 cookie 中
- 创建拦截器拦截请求,判断 cookie 是否有 token,如果有放到 请求头 中去
- 根据 token 获取用户信息,并将用户信息保存 cookie 中
- 首页显示用户信息
注意: 拦截器是拦截所有请求,但并不是阻止请求,他只是 判断 token,并放入 header 中去。
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',
})
},
}
import cookie from "js-cookie";
import loginApi from "~/api/login.js";
// 登录
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}"
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
HTML模板: 替换掉之前的 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 是 对象,不要忘记转换~~~~
效果:
用户信息保存到 cookie 中,只需要清空 cookie 即可
// 用户退出
logout() {
cookie.set("guli_token", '', { domain: 'localhost' })
cookie.set("userInfo",'', { domain: 'localhost' })
window.location.href = '/'
}
OAuth2 的定义:
其实就是给予应用有限的权限,代替用户访问用户的数据
那么是如何进行授权的呢?
OAuth2授权的核心就是 颁发 token 和使用 token
在 客户应用/ 第三方应用 向 授权服务器获取认证和授权后, 授权服务器为 客户应用/第三方 应用 颁发 token, 客户应用/ 第三方应用 拿着 token 去访问接口或者资源。
OAuth2 解决的问题:
- 开放系统间的授权
- 比如:下载一个游戏之后,需要授权手机的麦克风,相机等权限。这个就是授权。
- 分布式访问问题
- 类似单点登录
- 通过生成一定规则的字符串,保存到 cookie 中,其他服务判断 cookie 中是否有该字符串。
微信扫描登录就是使用的 OAuth2 授权…
# 微信开放平台 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;
}
}
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;
}
}
接口:
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
当扫描二维码后,会调用配置文件中的 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.httpcomponentsgroupId> <artifactId>httpclientartifactId> dependency> <dependency> <groupId>commons-iogroupId> <artifactId>commons-ioartifactId> dependency> <dependency> <groupId>com.google.code.gsongroupId> <artifactId>gsonartifactId> dependency>
- 复制 HttpClient 工具包 – 文档资料里有
实现代码:
@ApiOperation("获取扫描人的信息")
@GetMapping("callback")
private String getInfo(String code, String state) {
// 获取用户信息,并保存到 token 中
String token = memberService.callback(code,state);
return "redirect:http://localhost:3000?token=" + token;
}
接口:
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, "登陆失败");
}
}
实现步骤:
- 从路径中获取 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();
}
// 微信登录
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" });
});
},
点击名师,显示 名师 列表,每页显示八条数据
@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);
}
}
接口
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;
}
import request from '@/utils/request'
export default {
// 1.分页查询讲师列表
getTeacherList(page,limit) {
return request({
url: `/eduservice/teacherFront/pageTeacher/${page}/${limit}`,
method: 'post'
})
},
}
<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>
<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">
<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>
修改 讲师详情 的跳转链接:
@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);
}
// 2.查询讲师详情
getTeacherInfo(teacherid) {
return request({
url: `/eduservice/teacherFront/teacherInfoFront/${teacherid}`,
method: 'get'
})
},
<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>
<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>
根据以上的条件查询课程的信息,并且有选择的进行排序
并且查询出来的课程应该是已经发布的课程
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;
}
@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);
}
}
接口:
Map 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;
}
前端需要做的步骤:
- 查询出课程信息在页面中显示,带分页效果
- 查询出所有的 分类在页面中显示
- 点击 页码 进行跳转
- 点击 一级分类 显示对应的二级分类 以及 查询出对应的 课程信息
- 根据不同的条件进行排序
查询所有分类的方法,已经在 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
// 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
课程详情包含俩部分:
@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;
}
/**
* @description 查询课程详情
* @date 2022/9/6 21:59
* @param courseId
* @return java.util.List
*/
CourseWebVo getCourseFrontInfo(String courseId);
<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>
接口:
/**
* @description 查询课程详情
* @date 2022/9/6 21:59
* @param courseId
* @return java.util.List
*/
CourseWebVo getCourseFrontInfo(String courseId);
实现类:
@Override
public CourseWebVo getCourseFrontInfo(String courseId) {
return courseMapper.getCourseFrontInfo(courseId);
}
/**
* @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);
}
// 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">