• 谷粒学苑项目前台界面 (一)


    谷粒学苑项目前台界面

    项目分为三篇
    谷粒学苑项目前置知识
    谷粒学苑项目前台界面: 由于字数限制,分为俩部分,此篇为第一部分,第二部分
    谷粒学苑后台管理系统

    额外增加的功能:

    1. 后台 课程 小节的 删改 操作 🆗

    2. 课程列表的 分页查询和 条件查询 🆗

    3. 前台 banner 图的自动播放 🆗

    4. 后台 banner 的增删改 🆗

    5. 后台 对 前台轮播图的图片数量做一个设置。比如设置 5 张图片轮播,设置 3张图片轮播 🆗

    6. 课程详情 全部 按钮的实现 🆗

    7. 课程评论功能🆗

    资料链接:谷粒学苑
    提取码:p6er

    视频教程:尚硅谷-谷粒学苑

    前端代码:前端代码
    后端代码:后端代码

    部署方法

    一、使用 Nuxt 前台环境搭建

    将下载好的模板,里面的 template 放到 VSCode 中的工作区。

    image-20220828191217932

    image-20220828191324957

    在集成终端中打开该项目,使用 npm install 安装依赖

    启动:npm run dev

    image-20220828191623360

    启动之后有一些警告,是不影响运行的,只要不是 error 就行

    image-20220828191653088

    框架目录结构:

    (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 中只定义页面的头部,和尾部,中间引入其他组件

    image-20220828193158681

    二、前台首页静态显示

    1. 下载幻灯片插件
    npm install vue-awesome-swiper@3.1.3
    
    1. 在 plugins 目录下创建 nuxt-swiper-plugin.js,配置插件
    import Vue from 'vue'
    import VueAwesomeSwiper from 'vue-awesome-swiper/dist/ssr'
    
    Vue.use(VueAwesomeSwiper)
    
    1. 在 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'
      ]
    }
    

    image-20220828194029328

    1. 赋复静态资源到 项目assets 目录下

    image-20220828194214426

    1. 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>
              
              <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:info@atguigu.comspan>
                  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>
    
    1. index.vue 页面静态
      1. 轮播图实现了自动播放
    <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

    image-20220828195109643

    三、NUXT 中的路由

    1.固定路由

    路径是固定地址,不发生变化

    image-20220828234531394

    使用 标签实现跳转, to: 跳转的地址

    在 pages 中创建 course 文件夹,文件夹下创建 index.vue 页面,点击 课程 就会跳转到 /pages/course/index.vue 页面

    image-20220828234720073

    /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>
    

    image-20220828235333881

    2.动态路由

    路径是变化的,比如课程详情页面, 根据 ID 查询课程信息,不同的 ID 页面信息是不一样的。

    在 NUXT 中 动态路由的固定写法: _id.vue 必须以下划线开头,名字无所谓,但是最好见名知意。

    image-20220828235847209

    /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>
    
    

    四、封装 axios

    1. 安装 axios
    npm install axios
    
    1. 创建 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 缓存注解介绍:

    1. @Cacheable

    根据方法对其返回结果进行缓存,下次请求时,如果缓存存在,则直接读取缓存数据返回;如果缓存不存在,则执行方法,并把返回的结果存入缓存中。一般用在查询方法上

    属性/方法名解释
    value缓存名,必填,它指定了你的缓存存放在哪块命名空间
    cacheNames与 value 差不多,二选一即可
    key可选属性,可以使用 SpEL 标签自定义缓存的key

    @Cacheable 注解的流程:

    image-20220830115125331

    该注解是将方法返回值保存到缓存中,一般不建议在 controller 中用,一般都是在 serviceImpl 中使用该注解。

    1. @CachePut

    使用该注解标志的方法,每次都会执行,并将结果存入指定的缓存中。其他方法可以直接从响应的缓存中读取缓存数据,而不需要再去查询数据库。一般用在新增方法上

    属性/方法名解释
    value缓存名,必填,它指定了你的缓存存放在哪块命名空间
    cacheNames与 value 差不多,二选一即可
    key可选属性,可以使用 SpEL 标签自定义缓存的key
    1. @CacheEvict

    使用该注解标志的方法,会清空指定的缓存。一般用在更新或者删除方法上

    属性/方法名解释
    value缓存名,必填,它指定了你的缓存存放在哪块命名空间
    cacheNames与 value 差不多,二选一即可
    key可选属性,可以使用 SpEL 标签自定义缓存的key
    allEntries是否清空所有缓存 ,默认为 false。如果指定为 true,则方法调用后将立即清空所有的缓存
    beforeInvocation是否在方法执行前就清空,默认为 false。如果指定为 true,则在方法执行前就会清空缓存

    启动 Redis:

    1. 修改 redis.conf 配置文件
      1. 注释掉:#bind 127.0.0.1 -::1
      2. 关闭保护模式: protected-mode no
      3. 开启后台启动:daemonize yes
    2. 通过配置文件启动 redis-server //usr/local/redis/redis-6.2.1/redis.conf
    3. 进入命令行客户端: redis-cli -p 6379

    其他 Redis 操作,请查看 :https://blog.csdn.net/aetawt/article/details/126105301

    1.将banner存入 redis 缓存

    项目整合 Redis :

    1. 在其他模块也需要用 Redis,因此在 common 模块中引入依赖
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-data-redisartifactId>
            dependency>
    
            
            <dependency>
                <groupId>org.apache.commonsgroupId>
                <artifactId>commons-pool2artifactId>
            dependency>
    
    1. 在 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;
        }
    }
    
    1. 在 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
    
    1. 在 获取 banner图 的方法上增加 @Cacheable 注解

    image-20220830120833972

    注意: key 的 命名,需要一个双引号单引号

    1. 存入redis后的数据

    image-20220830121017815

    六、banner 模块

    1.环境搭建

    1. 新建 service_cms 模块
    2. 配置文件
    # 服务端口
    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
    
    1. 增加数据库表

    image-20220829001212452

    1. 使用 MyBatis 插件生成代码

    image-20220829002302150

    实体类中增加 自动填充 注解:

    1. 启动类
    @SpringBootApplication
    @MapperScan("com.atguigu.cms.mapper")
    @ComponentScan("com.atguigu")
    public class CmsMainApplication {
        public static void main(String[] args) {
            SpringApplication.run(CmsMainApplication.class,args);
        }
    }
    
    1. controller 层中创建俩个 controller,一个用于 后台管理,一个用于前台显示

    image-20220829153820698

    1. Nginx 中配置

    Nginx 配置文件增加请求转发路径:

    image-20220829172441885

    2.功能介绍

    需要做的功能:

    1. 后台对 banner 图的管理
      1. 显示所有banner图的列表
      2. 设置前台显示的 banner 图
      3. 增加 banner 图
      4. 删除 banner 图
    2. 前台显示 banner 图

    3.后台 banner 管理

    (1) 显示 banner 列表

    效果:

    image-20220830172117163

    点击图片可实现预览图片的效果:

    image-20220830172148536

    后台管理 – 后端设计:

    1. 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());
        }
        }
    

    后台管理 – 前端设计:

    1. 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' }
          }
        ]
      },
    
    
    1. 在 /src/views/edu/banner/ 目录下创建 list.vue , save.vue 页面

    image-20220830171752836

    1. 在 /src/api/front 目录下创建 banner.js ,用于定义访问接口的 api

    image-20220830172345169

    // 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',
            })
        },
        }
    
    1. 在 list.vue 页面定义模板
    <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 : 开启图片预览功能,参数是一个数组类型

    1. Js 代码部分
      1. 引入 banner.js 文件
      2. data 中定义数据
      3. created 中调用 方法
      4. 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 :

    image-20220830173051189

    修改完重新安装依赖: cnpm install

    (2) 后台设置前台显示的banner图

    效果演示:

    image-20220831171317508

    点击设置 弹出设置框:

    image-20220831171333997

    设置框里面的 banner 图,单击有预览效果

    里面的 banner 图是根据数据库关联的,勾选上哪个 banner 图,前台对应显示哪个 banner图。

    思路分析:

    1. 首先查询数据库在对话框中显示所有的 banner 图,这其实很简单。
    2. 使用 js 再点击确定的时候,判断有哪些banner 选中了,并将 这些 banner 图 的 ID 保存到一个集合中。
    3. 将 ID 集合 传到后端,后端根据这个 ID 集合 查询数据库,保存到 redis 缓存中
    4. 前台界面先从 redis 取 banner,如果有就直接取,没有查询数据库 ,显示 banner

    后台管理 – 后端设计:

    1. 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
         */
        @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;
        }
    }
    
    1. 后台管理 controller 层
      1. 注意:使用 @RequestBody 注解时,提交方式必须是 Post 。
        @ApiOperation(value = "修改前台banner图")
        @PostMapping("editFrontBannerCount")
        public R editFrontBannerCount(@RequestBody List<String> bannerIds) {
            bannerService.editFrontBannerCount(bannerIds);
            return R.ok();
        }
    

    后端管理 – 前端设计:

    1. 在 /src/api/front/banner.js 中定义访问接口的方法
          editBannerCount(bannerIds) {
            return request({
              url: `cmsService/cmsAdmin/editFrontBannerCount/`,
              method: 'post',
              data: bannerIds
            })
          }
    
    1. 在 /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 是打开对话框执行的回调函数

    1. data中定义数据
          // 设置轮播图
          dialogFormVisible: false,
          bannerIds: [], // 保存选中的图片的ID
    
    1. methods 编写方法,调用 banner.js 里面的方法

    editBanner 方法是点击 对话框 确定 执行的,一共需要做以下几件事:

    1. 在点击确定时,统计 对话框中勾选的 banner 图,并将 banner图的 ID 保存到集合中
    2. 提示框,点击确定 调用 banner.js 中的方法
    3. 重新刷新页面

    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 图需要做的事:

    1. 图片上传到 oss
    2. 图片的地址存到数据库

    后端设计:

    1. CmsAdminController ,增加 banner
        @ApiOperation(value = "新增Banner")
        @PostMapping("saveBanner")
        public R save(@RequestBody CrmBanner banner) {
            bannerService.save(banner);
            return R.ok();
        }
    

    前端设计:

    1. /src/api/front/banner.js 中定义增加banner 的方法
        // 3.增加 banner
        addBanner(banner) {
            return request({
                url: `cmsService/cmsAdmin/saveBanner/`,
                method: 'post',
                data: banner
            })
        },
    
    1. 增加页面模板
    <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>
    
    1. js 代码
      1. 引入 banner.js 文件
      2. data 中定义需要的数据
      3. methods 定义需要的方法
        1. 调用 banner.js 方法,实现增加banner
        2. handleAvatarSuccess 上传成功执行的方法,主要是获取图片的 url
        3. 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>
    
    1. 样式
    
    
    (4) 删除 banner 图

    后端设计:

    1. CMSAdminController 层,删除方法
        @ApiOperation(value = "删除Banner")
        @DeleteMapping("removeBanner/{id}")
        public R remove(@PathVariable String id) {
            bannerService.removeById(id);
            return R.ok();
        }
    

    前端设计:

    1. banner.js 中增加方法,
        // 4.删除 banner
        deleteBanner(bannerId) {
            return request({
                url: `cmsService/cmsAdmin/removeBanner/` + bannerId,
                method: 'delete',
            })
        },
    
    1. 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 取值是否 为空,空肯定没有设置,不为空就取出 值。

    前台显示 – 后端设计:

    1. 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);
        }
    }
    
    1. 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中一样,只不过有些目录需要自己创建

    1. 创建 api 目录,目录下创建 banner.js 文件,用于访问接口

    调用接口的方式 和后台管理系统中 一模一样

    import request from '@/utils/request'
    export default {
        // 1.获取 banner
      getList() {
        return request({
          url: `/cmsService/cmsFront/getAllBanner`,
          method: 'get'
        })
      }
    }
    
    1. 在 /pages/index.vue 中,data中定义数据
          // 保存 banner 图像
          bannerList: [],
    
    1. methods 中定义方法
      methods: {
        getBannerList() {
          banner.getList().then((response) => {
            // 这里和写后台管理不一样, 需要俩次 .data
            // 因为后台管理中,帮我们封装了一次 .data
            console.log(response.data.data.bannerList)
            this.bannerList = response.data.data.bannerList
          })
        },
      }
    
    1. created 中调用
      created() {
        this.getBannerList()
      },
    
    1. 幻灯片中使用 v-for 循环
            
            <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>
    
    1. 增加样式

    image-20220921102940288

    
    

    src:图片地址,我是将图片上传到 oss 中了,然后将图片地址保存到数据库中

    image-20220829182416859

    href : 点击图片跳转的路径

    七、前台课程,讲师列表显示

    1.后端

    在主页上,显示 8 条热门课程,和 4 名讲师,根据 ID 进行降序,使用 limit 限制个数。

    并且查询来的课程应该是已发布的课程,判断 数据库中 status 字段就行,Normal 为已发布

    image-20220829154112338

    image-20220829154122762

    在 service_edu 中 创建专门 编写 前台 controller 的包:

    image-20220829154243796

    @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.前端

    1. 在 api 目录下创建 course.js 文件定义访问接口的方法
    import request from '@/utils/request'
    export default {
        // 1.获取 课程 和 教师 列表
      getCourseTeacherList() {
        return request({
          url: `/eduservice/front/index`,
          method: 'get'
        })
      }
    }
    
    1. 在 /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 循环遍历

    插值语法只使用在 标签外部,标签内部使用 表达式应在 属性前加一个 :

    image-20220830095416567

          
          <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>
          
    

    八、前台登录

    登录方式介绍:

    单一服务器普通的登录方式:

    image-20220901160245391

    这样的登录方式,是在单一服务器上,但对于如今都是 分布式架构的项目,这种登录方式,就不行了。

    image-20220901160545308

    对于微服务架构的项目,就不在使用 单一服务器 的等方式,而是一种新的登录技术 —— 单点登录 【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 中
        • 放到地址栏上进行拼接,类似图片这样:
          • image-20220901164423545
      • 在其他模块中,通过地址栏上取到字符串(cookie),判断该字符串里是否有登录信息,有就是登录,没有就是未登录

    1.JWT 介绍

    在上面说了,token 是按照一定规则 生成的字符串,而 JWT 就是生成 token 字符串的 一种官方定义的规则。

    下面是按照 JWT生成的字符串:

    image-20220901165844293

    三种颜色分别对应三部分,用 . 分割

    1. JWT 头信息,JSON 格式

      1. {
          "alg": "HS256", // 加密的算法
          "typ": "JWT" // 令牌的类型,统一 JWT
        }
        
    2. 有效载荷部分(用户信息),是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据,JWT指定七个默认字段供选择。

    iss:发行人
    exp:到期时间
    sub:主题
    aud:用户
    nbf:在此之前不可用
    iat:发布时间
    jti:JWT ID用于标识该JWT

    
    除以上默认字段外,我们还可以自定义私有字段,如下例:
    
    ```json
    {
    "sub": "1234567890",
    "name": "Helen",
    "admin": true
    }
    
    1. 签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改 (防伪标志)。

      1. 首先,需要指定一个secret(秘钥,每个公司都不一样,自定义)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名。
      2. HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(claims), secret)

    2.项目整合 JWT

    1. 在 common 模块中引入 JWT 依赖
        <dependencies>
            
            <dependency>
                <groupId>io.jsonwebtokengroupId>
                <artifactId>jjwtartifactId>
            dependency>
        dependencies>
    
    1. 创建 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) 环境搭建
    1. 创建 service_msm 模块
    2. 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
    
    1. 启动类
    @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
    @ComponentScan("com.atguigu")
    public class MsmApplication {
        public static void main(String[] args) {
            SpringApplication.run(MsmApplication.class,args);
        }
    }
    
    1. 包结构

    image-20220901192123534

    (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={}

    1. msm 模块增加依赖
        <dependencies>
            <dependency>
                <groupId>com.alibabagroupId>
                <artifactId>fastjsonartifactId>
            dependency>
            <dependency>
                <groupId>com.aliyungroupId>
                <artifactId>aliyun-java-sdk-coreartifactId>
            dependency>
        dependencies>
    
    1. 引入工具类,该工具类随机生成 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;
    	}
    }
    
    
    1. 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;
        }
    
    1. 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("发送短信失败");
            }
        }
    }
    
    1. 使用 Swagger 测试

    image-20220904113144496

    4.用户登录/注册功能

    (1) 环境搭建
    1. 导入 sql 文件

    image-20220904113338634

    1. 创建 service_ucenter 模块,并使用 MyBatisX 插件生成代码

    image-20220904113940418

    1. 启动类
    @SpringBootApplication
    @ComponentScan("com.atguigu")
    @MapperScan("com.atguigu.ucenter.mapper")
    public class UcenterApplication {
        public static void main(String[] args) {
            SpringApplication.run(UcenterApplication.class,args);
        }
    }
    
    
    
    1. 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
    
    1. 实体类增加 @TableField 注解,实现自动填充

    image-20220904122354293

    (2) 登录 – 后端
    1. controller 层

    根据 手机号 和密码 登录,登录成功返回一个 token 信息

        @PostMapping("frontLogin")
        @ApiOperation("前台登录")
        private R loginUser(@RequestBody UcenterMember member) {
            // 登录成功返回一个 token,使用 jwt 生成
            String token = memberService.login(member);
            return R.ok().data("token", token);
        }
    
    1. service 层

    接口:

     String login(UcenterMember member);
    

    实现类:

    1. 判断手机号是否正确
    2. 判断密码是否正确
    3. 判断用户是否是禁用状态
        @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) 注册-- 后端
    1. 创建注册对象,保存注册的属性
    @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;
    }
    
    1. controller 层
        @PostMapping("frontRegister")
        @ApiOperation("前台注册")
        private R registerUser(@RequestBody RegisterVo registerVo) {
    
            return  memberService.register(registerVo);
        }
    
    1. service 层

    接口:

    R register(RegisterVo registerVo);
    

    实现类:

    1. 判断数据会否为空
    2. 判断验证码是否失效
    3. 判断注册的手机号是否重复
        @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) 前台登录、注册页面搭建
    1. 安装 element-ui 和 vue-qriously【微信支付生成二维码】和 npm install js-cookie【往cookie存token用】
    npm install element-ui
    npm install vue-qriously
    npm install js-cookie
    
    1. 修改 配置文件 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)
    
    1. 在 layouts 目录下创建 sign.vue
    <template>
      <div class="sign">
        
        <div class="logo">
          <img src="~/assets/img/logo.png" alt="logo">
        div>
        
        <nuxt/>
      div>
    template>
    
    1. 在 /layouts/default.vue 中修改登录注册的路径

    image-20220904143549939

    1. 在 pages 目录下创建 login.vue, register.vue

    image-20220904145203026

    1. 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>
    
    1. 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>
    
    

    image-20220904164155232

    • :rules 是框架帮我们做的校验规则,
      • required: 该输入框必须要填写内容
      • message: 不符合规则显示的信息
      • tigger : 触发机制,也就是什么时候出发
      • validator: 自己定义规则

    页面效果:

    image-20220904161744833

    (6) 注册 --前端
    1. 在 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
            })
          },
    
    }
    
    1. 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 增加请求转发:

    image-20220904162613326

    (7) 登录 – 前端
    image-20220904175406320
    1. 点击 登录 调用 后端接口 login 方法,返回 token 字符串
    2. 将 token 字符串保存到 cookie 中
    3. 创建拦截器拦截请求,判断 cookie 是否有 token,如果有放到 请求头 中去
    4. 根据 token 获取用户信息,并将用户信息保存 cookie 中
    5. 首页显示用户信息

    注意: 拦截器是拦截所有请求,但并不是阻止请求,他只是 判断 token,并放入 header 中去。

    1. 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',
            })
          },
    
    }
    
    1. login.vue 中引入 cookie.js 和 login.js
    import cookie from "js-cookie";
    import loginApi from "~/api/login.js";
    
    1. 实现登录方法
      • 第一步:调用登录方法,返回 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}"

    1. 实现第三步创建拦截器,拦截器是拦截所有请求,因此在 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
    
    1. 在 default.vue 页面显示用户信息

    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 是 对象,不要忘记转换~~~~

    效果:

    image-20220904202005698

    (8) 用户退出功能

    用户信息保存到 cookie 中,只需要清空 cookie 即可

        //  用户退出
        logout() {
           cookie.set("guli_token", '',  { domain: 'localhost' })
           cookie.set("userInfo",'', { domain: 'localhost' })
            window.location.href = '/'
        }
    

    5.微信扫描登录

    (1) OAuth2 介绍

    OAuth2 的定义:

    image-20220905134834665

    其实就是给予应用有限的权限,代替用户访问用户的数据

    那么是如何进行授权的呢?

    OAuth2授权的核心就是 颁发 token 和使用 token

    在 客户应用/ 第三方应用 向 授权服务器获取认证和授权后, 授权服务器为 客户应用/第三方 应用 颁发 token, 客户应用/ 第三方应用 拿着 token 去访问接口或者资源。

    OAuth2 解决的问题:

    1. 开放系统间的授权
      1. 比如:下载一个游戏之后,需要授权手机的麦克风,相机等权限。这个就是授权。
    2. 分布式访问问题
      1. 类似单点登录
      2. 通过生成一定规则的字符串,保存到 cookie 中,其他服务判断 cookie 中是否有该字符串。

    微信扫描登录就是使用的 OAuth2 授权…

    (2) 获取微信扫描二维码
    1. 配置文件中增加
      • 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 端口 !!!!

    image-20220905131008061

    1. 创建常量类,获取配置文件中的值
    @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;
        }
    }
    
    1. 创建 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;
        }
    }    
    
    1. 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;
        }
    

    image-20220905131317326

    效果: 访问:http://localhost:8160/api/ucenter/wx/login

    image-20220905131724910

    (3) 获取 扫描人 的信息

    当扫描二维码后,会调用配置文件中的 redirect_url 地址:http://localhost:8160/api/ucenter/wx/callback

    所以我们需要 将 controller 层的路径改成与之对应的,该地址不能修改,固定的格式。

    image-20220905135510139

    修改路径后不要忘记修改 Nginx 配置文件。

    官方提供的完整流程图:

    image-20220905141817054

    步骤分析:

    http://localhost:8160/api/ucenter/wx/callback?code=071bOp0w3n93aZ2Sy62w3ZLZNj1bOp0I&state=guli

    1. 在微信扫码之后,会将 code 和 state 拼接在地址栏上。获取到 code 和 state

      1. code: 临时票据,可以理解为 手机验证码
      2. state : 原样返回的值,防止 csrf 攻击
    2. 拿着 code 和 state 向 微信平台 提供的一个固定地址:https://api.weixin.qq.com/sns/oauth2/access_token 发送第一次请求, 返回的是含有 access_token , openId 的 JSON 串,我们可以使用Gson,fastjson 等工具 将 JSON 串转换成 Map 集合,实现 key-value存储

      1. access_token : 就是授权的令牌,访问凭证
      2. 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"
    
    }
    
    1. 拿着 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"
    }
    
    1. 获取到用户信息之后,需要在页面中进行显示,而之前的做法是将用户信息保存到 cookie 中,在首页 从 cookie 中取出数据

    image-20220905180121483

    而现在也可以这么做,但这么做有一个弊端: cookie 不能跨域

    因此我我们可以将 用户信息 使用 jwt 保存在 token 中,拼接在地址栏上,在首页进行获取。

    image-20220905181504402

    由于我们需要在 方法内部是实现 发送请求,因此需要使用 HttpClient 技术。

    1. 在 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>
    
    1. 复制 HttpClient 工具包 – 文档资料里有

    image-20220905152849441

    实现代码:

    1. 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;
        }
    
    1. 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) 前台首页显示用户信息

    实现步骤:

    1. 从路径中获取 token
    2. 将 token 存到 cookie 中
    3. 因为有拦截器,会判断 cookie 中是否有 token,如果有就将 token 保存到 请求 的 header 中去,并且 每次 请求 都会带有 header
    4. 根据 header 中的 token 获取用户信息,保存到 cookie 中。
    1. 在渲染页面之前,获取到路径中的参数

      • 注意: 路径参数 和 ? 参数获取的方式

      • 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();
        }
    
    1. 在 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.名师列表

    image-20220906083249570

    点击名师,显示 名师 列表,每页显示八条数据

    (1) 后端
    1. 在 service_edu 模块中,创建 TeacherFrontController 层

    image-20220906111121246

    @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);
        }
    }
    
    1. 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) 前端
    1. 在 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'
        })
      },
      }
    
    1. 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>
    
    1. 在无数据的时候,增加 v-if 指令 判断

    image-20220906111554489

    1. 删除多余的 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>
    
    1. 分页条
     
            <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>
            
    

    2.名师详情

    修改 讲师详情 的跳转链接:

    image-20220906114305623

    (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) 前端
    1. teacher.js 中定义 api
          // 2.查询讲师详情
          getTeacherInfo(teacherid) {
            return request({
              url: `/eduservice/teacherFront/teacherInfoFront/${teacherid}`,
              method: 'get'
            })
          },
    
    1. _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>
    
    1. 在无数据的时候,增加 v-if 指令 判断,课程数组长度等于0说明没有课程信息,

    image-20220906120656657

    1. 教师详情
    <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>
    
    1. 主讲课程信息
      
    
              <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) 后端

    image-20220906162159548

    根据以上的条件查询课程的信息,并且有选择的进行排序

    并且查询出来的课程应该是已经发布的课程

    image-20220908120121856

    status 为 Normal 的课程。

    1. 将以上条件封装成对象
    @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;
    
    }
    
    1. controller 层

    image-20220906171158914

    @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);
        }
    
    }
    
    1. service 层

    接口:

    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;
        }
    
    (2) 前端

    前端需要做的步骤:

    1. 查询出课程信息在页面中显示,带分页效果
    2. 查询出所有的 分类在页面中显示
    3. 点击 页码 进行跳转
    4. 点击 一级分类 显示对应的二级分类 以及 查询出对应的 课程信息
    5. 根据不同的条件进行排序
    1. 在 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";
    
    1. /pages/course/index.vue 页面中,data 中定义需要的数据
      data() {
        return {
          page: 1,
          limit: 8,
          data: {}, // 保存课程数据
          subjectNestedList: [], // 一级分类列表
          subSubjectList: [], // 二级分类列表
          searchObj: {}, // 查询表单对象
            
            // 以下数据是为了点击 条件 显示样式用的
          oneIndex: -1,
          twoIndex: -1,
          buyCountSort: "",
          gmtCreateSort: "",
          priceSort: "",
        };
      },
    
    1. 第一个功能:显示课程信息列表,并带分页。

      • 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"
                    >&lt;</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"
                    >&gt;</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;
                });
            },
        
    2. 第二个功能: 显示所有的一级分类

      • 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>
          
    3. 第三个功能:获取所有的二级分类在页面显示,并且根据一级分类查询课程信息

      • 点击 一级分类 显示对应的 二级分类,做成联动效果

      • 再点击 一级分类时,将 一级分类的 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>
        
    4. 第四个功能:点击二级分类,查询对应课程信息

      •     // 5.点击二级分类,进行查询
            searchTwoSubjectList(subjectId, index) {
              //  点击二级分类,显示选中效果
              this.twoIndex = index;
              // 将二级分类id赋值给条件查询对象
              this.searchObj.subjectId = subjectId;
              // 根据二级分类查询
              this.gotoPage(1, this.limit, this.searchObj);
            },
               
        
    5. 第五个功能:根据不同的条件进行排序

      • 点击哪个条件,就让哪个条件不为空 ,并封装到 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);
          },
      
    6. 第六个功能: 点击全部 显示所有的课程,并带上选中的样式,把其他选项样式去掉

      • 增加点击事件,并在 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) 后端

    image-20220906204740570

    image-20220906204803038

    课程详情包含俩部分:

    • 第一部分,是关于课程信息,需要查询多张表。课程表、讲师表、课程描述表…使用 SQL 的多表联查实现
    • 第二部分:是课程的章节信息,根据 courseId 查询即可,写过该方法。。。
    1. 对于课程详情的信息,使用一个 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;
    }
    
    
    1. EduCourseMapper
        /**
         * @description 查询课程详情
         * @date 2022/9/6 21:59
         * @param courseId
         * @return java.util.List
         */
        CourseWebVo getCourseFrontInfo(String courseId);
    
    1. 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>
    
    1. service 层

    接口:

        /**
         * @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);
        }
    
    1. 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) 前端
    1. 在 course.js 中定义 api
            // 4.查询课程详情
            getCourseDetailInfo(courseId) {
              return request({
                url: `/eduservice/courseFront/getCourseFrontInfo/` + courseId,
                method: 'get',
              })
            }
    
    1. 页面中做调用
    <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>
    
    1. 课程详情页面显示

    注意: 由于课程描述中增加了富文本编辑器,含有 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">
                          
                          <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>
    
  • 相关阅读:
    GitHub 最全的开发资源汇总系列
    删除不必要的内核模块
    C++中关于多线程并发访问实例函数与静态函数
    Redis的哨兵机制
    离散数学 学习 之 递推方程和生成函数
    2022 AI趋势8大预测
    和xshell和crt说再见,认识了一款51k star多端跨平台终端神器,强大酷炫
    [C++] 栈上存储以及堆中存储, 值引用, 类型拷贝以及拷贝构造函数, 将类分配到堆中
    8路高速光栅尺磁栅尺编码器4倍频计数转Modbus TCP网络模块 YL99-RJ45
    中国健身器材行业市场深度分析及发展规划咨询综合研究报告
  • 原文地址:https://blog.csdn.net/aetawt/article/details/127079875