谷粒学苑项目前台界面

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

额外增加的功能:

  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>
          <!-- / nav -->
          <ul class="h-r-login">
            <li id="no-login">
              <a href="/sing_in" title="登录">
                <em class="icon18 login-icon">&nbsp;</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">&nbsp;</em>
              </a>
              <q class="red-point" style="display: none">&nbsp;</q>
            </li>
            <li class="h-r-user undis" id="is-login-two">
              <a href="#" title>
                <img
                  src="~/assets/img/avatar-boy.gif"
                  width="30"
                  height="30"
                  class="vam picImg"
                  alt
                >
                <span class="vam disIb" id="userName"></span>
              </a>
              <a href="javascript:void(0)" title="退出" onclick="exit();" class="ml5">退出</a>
            </li>
            <!-- /未登录显示第1 li;登录后显示第2,3 li -->
          </ul>
          <aside class="h-r-search">
            <form action="#" method="post">
              <label class="h-r-s-box">
                <input type="text" placeholder="输入你想学的课程" name="queryCourse.courseName" value>
                <button type="submit" class="s-btn">
                  <em class="icon18">&nbsp;</em>
                </button>
              </label>
            </form>
          </aside>
        </div>
        <aside class="mw-nav-btn">
          <div class="mw-nav-icon"></div>
        </aside>
        <div class="clear"></div>
      </section>
    </header>
    <!-- /公共头引入 -->
      
    <nuxt/>

    <!-- 公共底引入 -->
    <footer id="footer">
      <section class="container">
        <div class>
          <h4 class="hLh30">
            <span class="fsize18 f-fM c-999">友情链接</span>
          </h4>
          <ul class="of flink-list">
            <li>
              <a href="http://www.atguigu.com/" title="尚硅谷" target="_blank">尚硅谷</a>
            </li>
          </ul>
          <div class="clear"></div>
        </div>
        <div class="b-foot">
          <section class="fl col-7">
            <section class="mr20">
              <section class="b-f-link">
                <a href="#" title="关于我们" target="_blank">关于我们</a>|
                <a href="#" title="联系我们" target="_blank">联系我们</a>|
                <a href="#" title="帮助中心" target="_blank">帮助中心</a>|
                <a href="#" title="资源下载" target="_blank">资源下载</a>|
                <span>服务热线:010-56253825(北京) 0755-85293825(深圳)</span>
                <span>Email:info@atguigu.com</span>
              </section>
              <section class="b-f-link mt10">
                <span>©2018课程版权均归谷粒学院所有 京ICP备17055252号</span>
              </section>
            </section>
          </section>
          <aside class="fl col-3 tac mt15">
            <section class="gf-tx">
              <span>
                <img src="~/assets/img/wx-icon.png" alt>
              </span>
            </section>
            <section class="gf-tx">
              <span>
                <img src="~/assets/img/wb-icon.png" alt>
              </span>
            </section>
          </aside>
          <div class="clear"></div>
        </div>
      </section>
    </footer>
    <!-- /公共底引入 -->
  </div>
</template>
<script>
import "~/assets/css/reset.css";
import "~/assets/css/theme.css";
import "~/assets/css/global.css";
import "~/assets/css/web.css";

export default {};
</script>
  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">1</i>/
              <i class="c-666 f-fM">1</i>
            </span>
          </section>
          <section class="fl">
            <ol class="js-tap clearfix">
              <li>
                <a title="关注度" href="#">关注度</a>
              </li>
              <li>
                <a title="最新" href="#">最新</a>
              </li>
              <li class="current bg-orange">
                <a title="价格" href="#">价格&nbsp;
                  <span></span>
                </a>
              </li>
            </ol>
          </section>
        </div>
        <div class="mt40">
          <!-- /无数据提示 开始-->
          <section class="no-data-wrap">
            <em class="icon30 no-data-ico">&nbsp;</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>&lt;</a>
            <a href="#" title class="current undisable">1</a>
            <a href="#" title>2</a>
            <a id="nextpage" href="#" title>&gt;</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.00</b>
            </section>
            <section class="c-attr-mt c-attr-undis">
              <span class="c-fff fsize14">主讲: 唐嫣&nbsp;&nbsp;&nbsp;</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>&nbsp;</p>
              <aside>
                <span class="c-fff f-fM">购买数</span>
                <br>
                <h6 class="c-fff f-fM mt10">150</h6>
              </aside>
            </li>
            <li>
              <p>&nbsp;</p>
              <aside>
                <span class="c-fff f-fM">课时数</span>
                <br>
                <h6 class="c-fff f-fM mt10">20</h6>
              </aside>
            </li>
            <li>
              <p>&nbsp;</p>
              <aside>
                <span class="c-fff f-fM">浏览数</span>
                <br>
                <h6 class="c-fff f-fM mt10">501</h6>
              </aside>
            </li>
          </ol>
        </aside>
        <div class="clear"></div>
      </div>
      <!-- /课程封面介绍 -->
      <div class="mt20 c-infor-box">
        <article class="fl col-7">
          <section class="mr30">
            <div class="i-box">
              <div>
                <section id="c-i-tabTitle" class="c-infor-tabTitle c-tab-title">
                  <a name="c-i" class="current" title="课程详情">课程详情</a>
                </section>
              </div>
              <article class="ml10 mr10 pt20">
                <div>
                  <h6 class="c-i-content c-infor-title">
                    <span>课程介绍</span>
                  </h6>
                  <div class="course-txt-body-wrap">
                    <section class="course-txt-body">
                      <p>
                        Java的发展历史,可追溯到1990年。当时Sun&nbsp;Microsystem公司为了发展消费性电子产品而进行了一个名为Green的项目计划。该计划
                        负责人是James&nbsp;Gosling。起初他以C++来写一种内嵌式软件,可以放在烤面包机或PAD等小型电子消费设备里,使得机器更聪明,具有人工智
                        能。但他发现C++并不适合完成这类任务!因为C++常会有使系统失效的程序错误,尤其是内存管理,需要程序设计师记录并管理内存资源。这给设计师们造成
                        极大的负担,并可能产生许多bugs。&nbsp;
                        <br>为了解决所遇到的问题,Gosling决定要发展一种新的语言,来解决C++的潜在性危险问题,这个语言名叫Oak。Oak是一种可移植性语言,也就是一种平台独立语言,能够在各种芯片上运行。
                        <br>1994年,Oak技术日趋成熟,这时网络正开始蓬勃发展。Oak研发小组发现Oak很适合作为一种网络程序语言。因此发展了一个能与Oak配合的浏
                        览器--WebRunner,后更名为HotJava,它证明了Oak是一种能在网络上发展的程序语言。由于Oak商标已被注册,工程师们便想到以自己常
                        享用的咖啡(Java)来重新命名,并于Sun&nbsp;World&nbsp;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">&nbsp;</em>第一节
                                </a>
                              </li>
                              <li class="lh-menu-second ml30">
                                <a href="#" title class="current-2">
                                  <em class="lh-menu-i-2 icon16 mr5">&nbsp;</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.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- spring2.X集成redis所需common-pool2-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
  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">
    <!-- banner列表表格 -->
    <el-table :data="list" border fit highlight-current-row>
      <el-table-column label="序号" width="70" align="center">
        <template slot-scope="scope">
          <!-- 计算序号的一个公式 -->
          {{ (current - 1) * limit + scope.$index + 1 }}
        </template>
      </el-table-column>

      <el-table-column prop="title" label="标题" width="80" />

      <!-- 图片 -->
      <el-table-column
        prop="imageUrl"
        label="图片预览"
        width="400"
        align="center"
      >
      <!-- 图片预览 -->
        <template width="90" slot-scope="scope">
          <div class="demo-image__preview">
            <el-image
              style="width: 200px; height: 100px"
              :src="scope.row.imageUrl"
              :preview-src-list="previewList"
            >
            </el-image>
          </div>
        </template>
      </el-table-column>

      <el-table-column prop="linkUrl" label="链接" width="100" align="center" />

      <el-table-column prop="gmtCreate" label="添加时间" width="160" />

      <el-table-column prop="sort" label="排序" width="60" align="center" />

      <el-table-column label="操作" align="center">
        <template slot-scope="scope">
          <el-button
            type="primary"
            size="mini"
            icon="el-icon-edit"
            @click="open(scope.row.id)"
            >修改</el-button
          >

          <el-button
            type="danger"
            size="mini"
            icon="el-icon-delete"
            @click="removeDataById(scope.row.id)"
            >删除</el-button
          >
        </template>
      </el-table-column>
    </el-table>

    <!-- 分页 -->
    <!-- Element-ui 会自动将这些值封装 -->
    <el-pagination
      :current-page="current"
      :page-size="limit"
      :total="total"
      style="padding: 30px 0; text-align: center"
      layout="total, prev, pager, next, jumper"
      @current-change="getBannerList"
    />
  </div>
</template>

:src : 图片源

:preview-src-list : 开启图片预览功能,参数是一个数组类型

  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<com.atguigu.cms.entity.CrmBanner>
     */
    @Override
    public List<CrmBanner> editFrontBannerCount(List<String> bannerIds) {
        // 创建 list 集合 保存banner 图
        List<CrmBanner> list = new ArrayList<>();
        for (String bannerId : bannerIds) {
            CrmBanner crmBanner = baseMapper.selectById(bannerId);
            if (crmBanner != null) {
                list.add(crmBanner);
            }
        }
        // 存入 redis,设置永不过期
        redisTemplate.opsForValue().set("editBannerList",list);
        return list;
    }
}
  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. 样式
<style>
.avatar-uploader .el-upload {
  border: 1px dashed #d9d9d9;
  border-radius: 6px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
}
.avatar-uploader .el-upload:hover {
  border-color: #409eff;
}
.avatar-uploader-icon {
  font-size: 28px;
  color: #8c939d;
  width: 178px;
  height: 178px;
  line-height: 178px;
  text-align: center;
}
.avatar {
  width: 400px;
  height: 200px;
  display: block;
}
</style>
(4) 删除 banner 图

后端设计:

  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 循环
        <!-- 遍历 bannerList -->
        <div v-for="banner in bannerList" :key="banner.id" class="swiper-slide" style="background: #040b1b">
          <a target="_blank" :href="banner.linkUrl">
            <img
              :src="banner.imageUrl"
              :alt="banner.title"
            />
          </a>
        </div>
  1. 增加样式

image-20220921102940288

<style scoped>
.img{
  width: 100%;
  height: 100%;
}
</style>

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>
        <!-- JWT-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
        </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&params={}

  1. msm 模块增加依赖
    <dependencies>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-core</artifactId>
        </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 列表

          <!-- / nav -->
          <ul class="h-r-login">
            <li v-if="!loginInfo.id" id="no-login">
              <a href="/login" title="登录">
                <em class="icon18 login-icon">&nbsp;</em>
                <span class="vam ml5">登录</span>
              </a>
              |
              <a href="/register" title="注册">
                <span class="vam ml5">注册</span>
              </a>
            </li>
            <li v-if="loginInfo.id" id="is-login-one" class="mr10">
              <a id="headerMsgCountId" href="#" title="消息">
                <em class="icon18 news-icon">&nbsp;</em>
              </a>
              <q class="red-point" style="display: none">&nbsp;</q>
            </li>
            <li v-if="loginInfo.id" id="is-login-two" class="h-r-user">
              <a href="/ucenter" title>
                <img
                  :src="loginInfo.avatar"
                  width="30"
                  height="30"
                  class="vam picImg"
                  alt
                />
                <span id="userName" class="vam disIb">{{
                  loginInfo.nickname
                }}</span>
              </a>
              <a
                href="javascript:void(0);"
                title="退出"
                @click="logout()"
                class="ml5"
                >退出</a
              >
            </li>
            <!-- /未登录显示第1 li;登录后显示第2,3 li -->
          </ul>

JS 代码:

<script>
import "~/assets/css/reset.css";
import "~/assets/css/theme.css";
import "~/assets/css/global.css";
import "~/assets/css/web.css";

import cookie from "js-cookie";
export default {
  data() {
    return {
      token: "",
      //  用户登录信息
      loginInfo: {
        id: "",
        age: "",
        avatar: "",
        mobile: "",
        nickname: "",
        sex: "",
      },
    };
  },
  created() {
    this.showInfo();
  },
  methods: {
    showInfo() {
      // 从 cookie 中取出数据
      var userInfo = cookie.get("userInfo");
    
      // 从 cookie 中取出的数据是 JSON格式 的字符串,需要转换成JSON对象
      if(userInfo) {
      this.loginInfo = JSON.parse(userInfo);
        console.log("首页:" + userInfo)
      }
    },
  },
};
</script>

再次提醒: cookie 中存储的是字符串,而 data 中定义的 loginInfo 是 对象,不要忘记转换~~~~

效果:

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.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
        </dependency>
        <!--commons-io-->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
        </dependency>
        <!--gson-->
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
        </dependency>
  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">
            <!-- 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>
        <!-- 公共分页 结束 -->

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}}&nbsp; {{ 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<String, Object> getCourseFrontList(long page, long limit, CourseFrontVo courseFrontVo);

实现类:

条件使用 condition 条件组装,比 if–else 真的省事。。

QueryWrapper 每组条件都有一个 condition 参数,该参数 是 boolean类型,如果为 true,则会组装后边的 条件,为 false,则不组装后面的 条件

    @Override
    public Map<String, Object> getCourseFrontList(long current, long limit, CourseFrontVo courseFrontVo) {
        Page<EduCourse> page = new Page<>(current,limit);

        QueryWrapper<EduCourse> queryWrapper = new QueryWrapper<>();
        // 组装条件查询
        queryWrapper.eq(!StringUtils.isEmpty(courseFrontVo.getSubjectParentId()) ,"subject_parent_id",courseFrontVo.getSubjectParentId())
                .eq( !StringUtils.isEmpty(courseFrontVo.getSubjectId()), "subject_id",courseFrontVo.getSubjectId())
                .orderByDesc(!StringUtils.isEmpty(courseFrontVo.getBuyCountSort()),"buy_count")
                .orderByDesc( !StringUtils.isEmpty(courseFrontVo.getPriceSort()),"price")
                .orderByDesc( !StringUtils.isEmpty(courseFrontVo.getGmtCreateSort()),"gmt_create")
                // 查询已发布的课程
                .eq("status","Normal");

        this.page(page,queryWrapper);

        List<EduCourse> records = page.getRecords();
        long pageCurrent = page.getCurrent();
        long pages = page.getPages();
        long size = page.getSize();
        long total = page.getTotal();
        boolean hasNext = page.hasNext();
        boolean hasPrevious = page.hasPrevious();

        Map<String, Object> map = new HashMap<String, Object>();
        map.put("items", records);
        map.put("current", pageCurrent);
        map.put("pages", pages);
        map.put("size", size);
        map.put("total", total);
        map.put("hasNext", hasNext);
        map.put("hasPrevious", hasPrevious);

        return map;
    }
(2) 前端

前端需要做的步骤:

  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()"
                        >价格&nbsp;
                        <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<com.atguigu.demo.entity.frontVo.CourseWebVo>
     */
    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<com.atguigu.demo.entity.frontVo.CourseWebVo>
     */
    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 }}&nbsp;&nbsp;&nbsp;</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>&nbsp;</p>
              <aside>
                <span class="c-fff f-fM">购买数</span>
                <br />
                <h6 class="c-fff f-fM mt10">{{ courseInfo.buyCount }}</h6>
              </aside>
            </li>
            <li>
              <p>&nbsp;</p>
              <aside>
                <span class="c-fff f-fM">课时数</span>
                <br />
                <h6 class="c-fff f-fM mt10">{{ courseInfo.lessonNum }}</h6>
              </aside>
            </li>
            <li>
              <p>&nbsp;</p>
              <aside>
                <span class="c-fff f-fM">浏览数</span>
                <br />
                <h6 class="c-fff f-fM mt10">{{ courseInfo.viewCount }}</h6>
              </aside>
            </li>
          </ol>
        </aside>
        <div class="clear"></div>
      </div>
      <!-- 课程介绍 -->
      <div class="mt20 c-infor-box">
        <article class="fl col-7">
          <section class="mr30">
            <div class="i-box">
              <div>
                <section id="c-i-tabTitle" class="c-infor-tabTitle c-tab-title">
                  <a name="c-i" class="current" title="课程详情">课程详情</a>
                </section>
              </div>
              <article class="ml10 mr10 pt20">
                <div>
                  <h6 class="c-i-content c-infor-title">
                    <span>课程介绍</span>
                  </h6>
                  <div class="course-txt-body-wrap">
                    <section class="course-txt-body">
                      <!-- v-html 将课程简介中的html标签解析 -->
                      <p v-html="courseInfo.description">
                        {{ courseInfo.description }}
                      </p>
                    </section>
                  </div>
                </div>
                <!-- 章节大纲 -->
                <div class="mt50">
                  <h6 class="c-g-content c-infor-title">
                    <span>课程大纲</span>
                  </h6>
                  <section class="mt20">
                    <div class="lh-menu-wrap">
                      <menu id="lh-menu" class="lh-menu mt10 mr10">
                        <ul>
                          <!-- 文件目录 -->
                          <li
                            class="lh-menu-stair"
                            v-for="chapter in chapterVideoList"
                            :key="chapter.id"
                          >
                            <a
                              href="javascript: void(0)"
                              title="第一章"
                              class="current-1"
                            >
                              <em class="lh-menu-i-1 icon18 mr10"></em
                              >{{ chapter.title }}
                            </a>
                            <ol class="lh-menu-ol" style="display: block">
                              <li
                                class="lh-menu-second ml30"
                                v-for="video in chapter.children"
                                :key="video.id"
                              >
                                <a href="#" title>
                                  <span v-if="Number(courseInfo.price) === 0" class="fr">
                                    <i class="free-icon vam mr10">免费试听</i>
                                  </span>
                                  <em class="lh-menu-i-2 icon16 mr5">&nbsp;</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>
Logo

前往低代码交流专区

更多推荐