目录

1.初始课程详情页面

2.视频播放组件

3.课程详情页面后端接口实现

4.课程详情页面-前端

5.CKEditor富文本编辑器

6.课程章节和课时显示-后端接口

7.课程章节和课时显示-前端

1.初始课程详情页面

1.Detail.vue

  1. <!-- 课程详情页面初始页面 -->
  2. <template>
  3. <div class="detail">
  4. <Vheader/>
  5. <div class="main">
  6. <div class="course-info">
  7. <div class="wrap-left">
  8.  
  9. </div>
  10. <div class="wrap-right">
  11. <h3 class="course-name">flask</h3>
  12. <p class="data">111人在学&nbsp;&nbsp;&nbsp;&nbsp;课程总时长:111课时/12小时&nbsp;&nbsp;&nbsp;&nbsp;难度:</p>
  13. <div class="sale-time">
  14. <p class="sale-type">限时免费</p>
  15. <p class="expire">距离结束:仅剩 01天 04小时 33分 <span class="second">08</span></p>
  16. </div>
  17. <p class="course-price">
  18. <span>活动价</span>
  19. <span class="discount">¥0.00</span>
  20. <span class="original">¥1111</span>
  21. </p>
  22. <div class="buy">
  23. <div class="buy-btn">
  24. <button class="buy-now">立即购买</button>
  25. <button class="free">免费试学</button>
  26. </div>
  27. <div class="add-cart"><img src="/static/img/cart-yellow.svg" alt="">加入购物车</div>
  28. </div>
  29. </div>
  30. </div>
  31. <div class="course-tab">
  32. <ul class="tab-list">
  33. <li :class="tabIndex==1?'active':''" @click="tabIndex=1">详情介绍</li>
  34. <li :class="tabIndex==2?'active':''" @click="tabIndex=2">课程章节 <span :class="tabIndex!=2?'free':''">(试学)</span></li>
  35. <li :class="tabIndex==3?'active':''" @click="tabIndex=3">用户评论 (42)</li>
  36. <li :class="tabIndex==4?'active':''" @click="tabIndex=4">常见问题</li>
  37. </ul>
  38. </div>
  39. <div class="course-content">
  40. <div class="course-tab-list">
  41. <div class="tab-item" v-if="tabIndex==1">
  42. <div class="course-brief" v-html=""></div>
  43. </div>
  44. <div class="tab-item" v-if="tabIndex==2">
  45. <div class="tab-item-title">
  46. <p class="chapter">课程章节</p>
  47. <p class="chapter-length">共11章 147个课时</p>
  48. </div>
  49. <div class="chapter-item">
  50. <p class="chapter-title"><img src="/static/img/1.png" alt="">第1章·Linux硬件基础</p>
  51. <ul class="lesson-list">
  52. <li class="lesson-item">
  53. <p class="name"><span class="index">1-1</span> 课程介绍-学习流程<span class="free">免费</span></p>
  54. <p class="time">07:30 <img src="/static/img/chapter-player.svg"></p>
  55. <button class="try">立即试学</button>
  56. </li>
  57. <li class="lesson-item">
  58. <p class="name"><span class="index">1-2</span> 服务器硬件-详解<span class="free">免费</span></p>
  59. <p class="time">07:30 <img src="/static/img/chapter-player.svg"></p>
  60. <button class="try">立即试学</button>
  61. </li>
  62. </ul>
  63. </div>
  64. <div class="chapter-item">
  65. <p class="chapter-title"><img src="/static/img/1.png" alt="">第2章·Linux发展过程</p>
  66. <ul class="lesson-list">
  67. <li class="lesson-item">
  68. <p class="name"><span class="index">2-1</span> 操作系统组成-Linux发展过程</p>
  69. <p class="time">07:30 <img src="/static/img/chapter-player.svg"></p>
  70. <button class="try">立即购买</button>
  71. </li>
  72. <li class="lesson-item">
  73. <p class="name"><span class="index">2-2</span> 自由软件-GNU-GPL核心讲解</p>
  74. <p class="time">07:30 <img src="/static/img/chapter-player.svg"></p>
  75. <button class="try">立即购买</button>
  76. </li>
  77. </ul>
  78. </div>
  79. </div>
  80. <div class="tab-item" v-if="tabIndex==3">
  81. 用户评论
  82. </div>
  83. <div class="tab-item" v-if="tabIndex==4">
  84. 常见问题
  85. </div>
  86. </div>
  87. <div class="course-side">
  88. <div class="teacher-info">
  89. <h4 class="side-title"><span>授课老师</span></h4>
  90. <div class="teacher-content">
  91. <div class="cont1">
  92. <img src="">
  93. <div class="name">
  94. <p class="teacher-name">xxx</p>
  95. <p class="teacher-title">ssss</p>
  96. </div>
  97. </div>
  98. <p class="narrative" >kkkk</p>
  99. </div>
  100. </div>
  101. </div>
  102. </div>
  103. </div>
  104. <Footer/>
  105. </div>
  106. </template>
  107.  
  108. <script>
  109. import Vheader from "./common/Vheader"
  110. import Footer from "./common/Footer"
  111.  
  112. export default {
  113. name: "Detail",
  114. data(){
  115. return {
  116. tabIndex:1,
  117. }
  118. },
  119. created(){
  120.  
  121. },
  122. methods: {
  123.  
  124. },
  125. components:{
  126. Vheader,
  127. Footer,
  128.  
  129. }
  130. }
  131. </script>
  132.  
  133. <style scoped>
  134. .main{
  135. background: #fff;
  136. padding-top: 30px;
  137. }
  138. .course-info{
  139. width: 1200px;
  140. margin: 0 auto;
  141. overflow: hidden;
  142. }
  143. .wrap-left{
  144. float: left;
  145. width: 690px;
  146. height: 388px;
  147. background-color: #000;
  148. }
  149. .wrap-right{
  150. float: left;
  151. position: relative;
  152. height: 388px;
  153. }
  154. .course-name{
  155. font-size: 20px;
  156. color: #333;
  157. padding: 10px 23px;
  158. letter-spacing: .45px;
  159. }
  160. .data{
  161. padding-left: 23px;
  162. padding-right: 23px;
  163. padding-bottom: 16px;
  164. font-size: 14px;
  165. color: #9b9b9b;
  166. }
  167. .sale-time{
  168. width: 464px;
  169. background: #fa6240;
  170. font-size: 14px;
  171. color: #4a4a4a;
  172. padding: 10px 23px;
  173. overflow: hidden;
  174. }
  175. .sale-type {
  176. font-size: 16px;
  177. color: #fff;
  178. letter-spacing: .36px;
  179. float: left;
  180. }
  181. .sale-time .expire{
  182. font-size: 14px;
  183. color: #fff;
  184. float: right;
  185. }
  186. .sale-time .expire .second{
  187. width: 24px;
  188. display: inline-block;
  189. background: #fafafa;
  190. color: #5e5e5e;
  191. padding: 6px 0;
  192. text-align: center;
  193. }
  194. .course-price{
  195. background: #fff;
  196. font-size: 14px;
  197. color: #4a4a4a;
  198. padding: 5px 23px;
  199. }
  200. .discount{
  201. font-size: 26px;
  202. color: #fa6240;
  203. margin-left: 10px;
  204. display: inline-block;
  205. margin-bottom: -5px;
  206. }
  207. .original{
  208. font-size: 14px;
  209. color: #9b9b9b;
  210. margin-left: 10px;
  211. text-decoration: line-through;
  212. }
  213. .buy{
  214. width: 464px;
  215. padding: 0px 23px;
  216. position: absolute;
  217. left: 0;
  218. bottom: 20px;
  219. overflow: hidden;
  220. }
  221. .buy .buy-btn{
  222. float: left;
  223. }
  224. .buy .buy-now{
  225. width: 125px;
  226. height: 40px;
  227. border: 0;
  228. background: #ffc210;
  229. border-radius: 4px;
  230. color: #fff;
  231. cursor: pointer;
  232. margin-right: 15px;
  233. outline: none;
  234. }
  235. .buy .free{
  236. width: 125px;
  237. height: 40px;
  238. border-radius: 4px;
  239. cursor: pointer;
  240. margin-right: 15px;
  241. background: #fff;
  242. color: #ffc210;
  243. border: 1px solid #ffc210;
  244. }
  245. .add-cart{
  246. float: right;
  247. font-size: 14px;
  248. color: #ffc210;
  249. text-align: center;
  250. cursor: pointer;
  251. margin-top: 10px;
  252. }
  253. .add-cart img{
  254. width: 20px;
  255. height: 18px;
  256. margin-right: 7px;
  257. vertical-align: middle;
  258. }
  259.  
  260. .course-tab{
  261. width: 100%;
  262. background: #fff;
  263. margin-bottom: 30px;
  264. box-shadow: 0 2px 4px 0 #f0f0f0;
  265.  
  266. }
  267. .course-tab .tab-list{
  268. width: 1200px;
  269. margin: auto;
  270. color: #4a4a4a;
  271. overflow: hidden;
  272. }
  273. .tab-list li{
  274. float: left;
  275. margin-right: 15px;
  276. padding: 26px 20px 16px;
  277. font-size: 17px;
  278. cursor: pointer;
  279. }
  280. .tab-list .active{
  281. color: #ffc210;
  282. border-bottom: 2px solid #ffc210;
  283. }
  284. .tab-list .free{
  285. color: #fb7c55;
  286. }
  287. .course-content{
  288. width: 1200px;
  289. margin: 0 auto;
  290. background: #FAFAFA;
  291. overflow: hidden;
  292. padding-bottom: 40px;
  293. }
  294. .course-tab-list{
  295. width: 880px;
  296. height: auto;
  297. padding: 20px;
  298. background: #fff;
  299. float: left;
  300. box-sizing: border-box;
  301. overflow: hidden;
  302. position: relative;
  303. box-shadow: 0 2px 4px 0 #f0f0f0;
  304. }
  305. .tab-item{
  306. width: 880px;
  307. background: #fff;
  308. padding-bottom: 20px;
  309. box-shadow: 0 2px 4px 0 #f0f0f0;
  310. }
  311. .tab-item-title{
  312. justify-content: space-between;
  313. padding: 25px 20px 11px;
  314. border-radius: 4px;
  315. margin-bottom: 20px;
  316. border-bottom: 1px solid #333;
  317. border-bottom-color: rgba(51,51,51,.05);
  318. overflow: hidden;
  319. }
  320.  
  321. .chapter{
  322. font-size: 17px;
  323. color: #4a4a4a;
  324. float: left;
  325. }
  326. .chapter-length{
  327. float: right;
  328. font-size: 14px;
  329. color: #9b9b9b;
  330. letter-spacing: .19px;
  331. }
  332. .chapter-title{
  333. font-size: 16px;
  334. color: #4a4a4a;
  335. letter-spacing: .26px;
  336. padding: 12px;
  337. background: #eee;
  338. border-radius: 2px;
  339. display: -ms-flexbox;
  340. display: flex;
  341. -ms-flex-align: center;
  342. align-items: center;
  343. }
  344. .chapter-title img{
  345. width: 18px;
  346. height: 18px;
  347. margin-right: 7px;
  348. vertical-align: middle;
  349. }
  350. .lesson-list{
  351. padding:0 20px;
  352. }
  353. .lesson-list .lesson-item{
  354. padding: 15px 20px 15px 36px;
  355. cursor: pointer;
  356. justify-content: space-between;
  357. position: relative;
  358. overflow: hidden;
  359. }
  360. .lesson-item .name{
  361. font-size: 14px;
  362. color: #666;
  363. float: left;
  364. }
  365. .lesson-item .index{
  366. margin-right: 5px;
  367. }
  368. .lesson-item .free{
  369. font-size: 12px;
  370. color: #fff;
  371. letter-spacing: .19px;
  372. background: #ffc210;
  373. border-radius: 100px;
  374. padding: 1px 9px;
  375. margin-left: 10px;
  376. }
  377. .lesson-item .time{
  378. font-size: 14px;
  379. color: #666;
  380. letter-spacing: .23px;
  381. opacity: 1;
  382. transition: all .15s ease-in-out;
  383. float: right;
  384. }
  385. .lesson-item .time img{
  386. width: 18px;
  387. height: 18px;
  388. margin-left: 15px;
  389. vertical-align: text-bottom;
  390. }
  391. .lesson-item .try{
  392. width: 86px;
  393. height: 28px;
  394. background: #ffc210;
  395. border-radius: 4px;
  396. font-size: 14px;
  397. color: #fff;
  398. position: absolute;
  399. right: 20px;
  400. top: 10px;
  401. opacity: 0;
  402. transition: all .2s ease-in-out;
  403. cursor: pointer;
  404. outline: none;
  405. border: none;
  406. }
  407. .lesson-item:hover{
  408. background: #fcf7ef;
  409. box-shadow: 0 0 0 0 #f3f3f3;
  410. }
  411. .lesson-item:hover .name{
  412. color: #333;
  413. }
  414. .lesson-item:hover .try{
  415. opacity: 1;
  416. }
  417.  
  418. .course-side{
  419. width: 300px;
  420. height: auto;
  421. margin-left: 20px;
  422. float: right;
  423. }
  424. .teacher-info{
  425. background: #fff;
  426. margin-bottom: 20px;
  427. box-shadow: 0 2px 4px 0 #f0f0f0;
  428. }
  429. .side-title{
  430. font-weight: normal;
  431. font-size: 17px;
  432. color: #4a4a4a;
  433. padding: 18px 14px;
  434. border-bottom: 1px solid #333;
  435. border-bottom-color: rgba(51,51,51,.05);
  436. }
  437. .side-title span{
  438. display: inline-block;
  439. border-left: 2px solid #ffc210;
  440. padding-left: 12px;
  441. }
  442.  
  443. .teacher-content{
  444. padding: 30px 20px;
  445. box-sizing: border-box;
  446. }
  447.  
  448. .teacher-content .cont1{
  449. margin-bottom: 12px;
  450. overflow: hidden;
  451. }
  452.  
  453. .teacher-content .cont1 img{
  454. width: 54px;
  455. height: 54px;
  456. margin-right: 12px;
  457. float: left;
  458. }
  459. .teacher-content .cont1 .name{
  460. float: right;
  461. }
  462. .teacher-content .cont1 .teacher-name{
  463. width: 188px;
  464. font-size: 16px;
  465. color: #4a4a4a;
  466. padding-bottom: 4px;
  467. }
  468. .teacher-content .cont1 .teacher-title{
  469. width: 188px;
  470. font-size: 13px;
  471. color: #9b9b9b;
  472. white-space: nowrap;
  473. }
  474. .teacher-content .narrative{
  475. font-size: 14px;
  476. color: #666;
  477. line-height: 24px;
  478. }
  479. </style>

初始课程详情页面

2.index.js注册组件

  1. import Detail from "@/components/Detail"
  2. {
  3. path:'/course/detail/:id', // 前端页面动态路由匹配
  4. component:Detail
  5. }
  6.  
  7. // :id ===> this.$route.params.id // course/detail/1

3.course.vue

实现:在课程列表页面点击不同的课程可以进入到不同的课程详情页面

  1. <h3><router-link :to="'/course/detail/'+course.id+'/'">django基础知识</router-link> <span><img src="/static/img/avatar1.svg" alt="">5000人已加入学习</span></h3>

此时 点击可进入课程详情页面

2.视频播放组件

1.安装

  1. npm install vue-video-player --save

2.main.js注册组件

  1. // main.js
  2.  
  3. require('video.js/dist/video-js.css');
  4. require('vue-video-player/src/custom-theme.css');
  5. import VideoPlayer from 'vue-video-player'
  6. Vue.use(VideoPlayer);

3.Detail.vue引入

HTML部分

  1. <!-- html -->
  2. <div class="wrap-left">
  3. <videoPlayer class="video-player vjs-custom-skin"
  4. ref="videoPlayer"
  5. :playsinline="true"
  6. :options="playerOptions"
  7. @play="onPlayerPlay($event)"
  8. @pause="onPlayerPause($event)">
  9.  
  10. </videoPlayer>
  11. </div>

JS部分

  1. // js
  2. import {VideoPlayer} from 'vue-video-player'
  3. data(){
  4. return{
  5. ...
  6. playerOptions: {
  7. playbackRates: [0.7, 1.0, 1.5, 2.0], // 播放速度
  8. autoplay: false, // 如果true,则自动播放
  9. muted: false, // 默认情况下将会消除任何音频。
  10. loop: false, // 循环播放
  11. preload: 'auto', // 建议浏览器在<video>加载元素后是否应该开始下载视频数据。auto浏览器选择最佳行为,立即开始加载视频(如果浏览器支持)
  12. language: 'zh-CN',
  13. aspectRatio: '16:9', // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3")
  14. fluid: true, // 当true时,Video.js player将拥有流体大小。换句话说,它将按比例缩放以适应其容器。
  15. sources: [{ // 播放资源和资源格式
  16. type: "video/mp4",
  17. src: "" // 你的视频地址(必填)
  18. }],
  19. poster: "", // 视频封面图
  20. width: document.documentElement.clientWidth, // 默认视频全屏时的最大宽度
  21. notSupportedMessage: '此视频暂无法播放,请稍后再试', // 允许覆盖Video.js无法播放媒体源时显示的默认信息。
  22. }
  23.  
  24. }
  25.  
  26. }
  27.  
  28. method:{
  29. ...
  30. // 视频播放时触发此函数
  31. onPlayerPlay:{
  32. ...
  33. }
  34. // 视频暂停时触发此函数
  35. onPlayerPause:{
  36. ...
  37. }
  38. }
  39. components:{
  40. ...
  41. videoplayer // 挂载一下视频播放组件
  42. }

4.在Xadmin上传视频

注意:课程详情页的那些视频其实要存到数据库里的,但是数据库并没有课程视频这个字段

所以需要在course表中添加一个course_video字段

  1. # 将上传的视频保存在本地的video文件夹中
  2. course_video = models.FileField(upload_to='video',verbose_name='封面视频',blank=True,null=True,max_length=255)

执行数据库迁移指令

  1. python3 manage.py makemigrations
  2. python3 manage.py migrate

在xadmin上传视频,即可在前端页面看到自己上传的视频

3.课程详情页面后端接口实现

接下来做的事情:在课程表里已经有了我们视频数据了,现在我们要想办法写一个后端接口将真实的课程详情数据返回到页面上,把真实的视频播放路径给前端,让前端展示出来。把真实的图片路径或者视频路径给前端,前端加载的时候会往后端发请求获取地址对应的视频数据,然后进行播放就可以了

urls.py

  1. urlpatterns = [
  2. ......
  3. re_path(r'detail/(?P<pk>\d+)/', views.CourseDetailView.as_view(),),
  4.  
  5. ]

views.py

  1. class CourseDetailView(RetrieveAPIView):
  2. queryset = models.Course.objects.filter(is_deleted=False,is_show=True)
  3. serializer_class = CourseDetailModelSerializer

models.py

  1. class Course(BaseModel):
  2. ......
  3. level_choices = (
  4. (0, '初级'),
  5. (1, '中级'),
  6. (2, '高级'),
  7. )
  8. ......
  9. ......
  10.  
  11. level = models.SmallIntegerField(choices=level_choices, default=1, verbose_name="难度等级")
  12.  
  13. ......
  14.  
  15. def level_name(self):
  16. '''level字段默认显示的是数字,通过返回get_字段_display可以返回数字对应的名字'''
  17. return self.get_level_display()

serializers.py

  1. class CourseDetailModelSerializer(serializers.ModelSerializer):
  2. # 序列化器嵌套
  3. teacher = TeacherModelSerializer() # 将外键关联的属性指定为关联表的序列化器对象,就能拿到关联表序列化出来的所有数据,还需要在fields中指定一下,注意,名称必须和外键属性名称相同
  4.  
  5. class Meta:
  6. model = models.Course
  7. fields = ["id", "name", "course_img", "students", "lessons", "pub_lessons", "price", "teacher",
  8. "level_name", "course_video"]

后端接口测试

drf后端接口测试 /course/detail/1 可得到course=1所需要的所有数据

4.课程详情页面-前端

1.注意点

在课程列表页面,我们只展示了四个课时。但是在课程详情页我们要展示所有课时,所以不能用之前那个序列化器了。

现在我们是需要所有章节和所有课时信息、老师信息和课程信息。

如果将所有的信息都定义到一个序列化器的字段中,数据量有些太大。

我们可以利用axios可以发送异步请求的这个特点,分成两次请求来获取数据

将章节信息和课时信息放在一个序列化器中

其它的放在另一个序列化器中

我们先去请求除了章节信息和课时信息的其他信息

现在后端数据已经准备好了,接下来就是前端发送axios请求获取数据了

2.Detail.vue

  1. <!-- html -->
  2. <div class="wrap-right">
  3. <h3 class="course-name">{{ course_data.name }}</h3>
  4. <p class="data">{{course_data.students}}人在学&nbsp;&nbsp;&nbsp;&nbsp;课程总时长:{{course_data.lessons}}&nbsp;&nbsp;&nbsp;&nbsp;难度:{{course_data.level_name}}</p>
  5. <div class="sale-time">
  6. <p class="sale-type">限时免费</p>
  7. <p class="expire">距离结束:仅剩 01天 04小时 33分 <span class="second">08</span></p>
  8. </div>
  9. <p class="course-price">
  10. <span>活动价</span>
  11. <span class="discount">¥0.00</span>
  12. <span class="original">¥{{course_data.price}}</span>
  13. </p>
  14. <div class="buy">
  15. <div class="buy-btn">
  16. <button class="buy-now">立即购买</button>
  17. <button class="free">免费试学</button>
  18. </div>
  19. <div class="add-cart"><img src="/static/img/cart-yellow.svg" alt="">加入购物车</div>
  20. </div>
  21. </div>
  22.  
  23. <!-- 老师部分 -->
  24. <div class="course-side">
  25. <div class="teacher-info">
  26. <h4 class="side-title"><span>授课老师</span></h4>
  27. <div class="teacher-content">
  28. <div class="cont1">
  29. <img src="">
  30. <div class="name">
  31. <p class="teacher-name">{{course_data.teacher.name}}</p>
  32. <p class="teacher-title">{{course_data.teacher.title}}</p>
  33. </div>
  34. </div>
  35. <p class="narrative" >{{course_data.teacher.signature}}</p>
  36. </div>
  37. </div>
  38. </div>
  39.  
  40. <!-- 视频播放 -->
  41. <div class="wrap-left">
  42. <videoPlayer class="video-player vjs-custom-skin"
  43. ref="videoPlayer"
  44. :playsinline="true"
  45. :options="playerOptions"
  46. @play="onPlayerPlay($event)"
  47. @pause="onPlayerPause($event)">
  48. </videoPlayer>
  49. </div>

前端将从后段获取的数据展示出来-HTML

  1. // js
  2.  
  3. <script>
  4.  
  5. export default {
  6. name: "Detail",
  7. data(){
  8. return {
  9. ......
  10. course_id:0,
  11. course_data:{
  12. teacher:{}
  13. },
  14. playerOptions: {
  15. ......
  16. sources: [{ // 播放资源和资源格式
  17. type: "video/mp4",
  18. src: "" // 你的视频地址(必填)
  19. }],
  20. poster: "", // 视频封面图
  21. ......
  22.  
  23. },
  24. created(){
  25. this.get_course_id();
  26. this.get_course_data();
  27. },
  28. methods: {
  29.  
  30. // 获取课程id,用处是请求不同的课程详情页面数据时带上不同的url参数来请求不同的课程详情数据
  31. get_course_id(){
  32. this.course_id = this.$route.params.id;
  33. // 可以判断course_id的合法性 todo
  34. },
  35.  
  36. get_course_data(){
  37. this.$axios.get(`${this.$settings.Host}/course/detail/${this.course_id}/`)
  38. .then((res)=>{
  39. this.course_data = res.data; // 获取课程详情页数据
  40. this.playerOptions.sources[0].src = res.data.course_video; // 获取视频数据
  41. this.playerOptions.poster = res.data.course_img // 获取视频封面数据
  42.  
  43. })
  44. },
  45.  
  46. },
  47.  
  48. }
  49. </script>

前端发送axios请求获取后端数据-JS

5.CKEditor富文本编辑器

1.安装

  1. pip install django-ckeditor

2.settings/dev.py INSTALLAPP配置

  1. INSTALLED_APPS = [
  2. ...
  3. 'ckeditor', # 富文本编辑器
  4. 'ckeditor_uploader', # 富文本编辑器上传图片模块
  5. ...
  6. ]

3.setting/dev.py 配置

  1. # 富文本编辑器ckeditor配置
  2. CKEDITOR_CONFIGS = {
  3. 'default': {
  4. 'toolbar': 'full', # 工具条功能,full表示全部,Basic表示基本功能,功能少很多,还有个Custom自定义功能选项
  5. 'height': 300, # 编辑器高度
  6. # 'width': 300, # 编辑器宽
  7. },
  8. }
  9. CKEDITOR_UPLOAD_PATH = '' # 上传图片保存路径,留空则调用django的文件上传功能
  10.  
  11. # 也可以自定义配置
  12. CKEDITOR_CONFIGS = {
  13. 'default': {
  14. 'toolbar': 'Custom',
  15. 'toolbar_Custom': [
  16. ['Bold', 'Italic', 'Underline','Image'], # 通过浏览器f12来查看每个功能的标签,就看到了类值cke_button_工具名称[注意使用驼峰式来写]
  17. ['NumberedList', 'BulletedList', '-', 'Outdent', 'Indent', '-', 'JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock'],
  18. ['Link', 'Unlink'],
  19. ['RemoveFormat', 'Source']
  20. ]
  21. }
  22. }

4.在总路由lyapi/urls.py添加路由

  1. path(r'^ckeditor/', include('ckeditor_uploader.urls')),

5.将brief字段升级

  1. # course/models.py
  2. from ckeditor_uploader.fields import RichTextUploadingField
  3. class Course(models.Model):
  4.  
  5. # 课程概述变为富文本编辑器显示
  6. brief = RichTextUploadingField(max_length=2048, verbose_name="课程概述", null=True, blank=True)

6.brief图片路径转化问题

相对路径转化为绝对路径

在brief中,存放的都是一些各种标签组成的字符串,而用户在使用富文本编辑器时,有可能会使用上传图片的功能。而图片上传后,默认都存在了后端的media文件夹中。但是前端并不会将我们的后端地址识别出来。它会默认被存放到前端:www.lycity.com/media中,所以需要我们手动更改一下上传图片存储的路径。这样用户上传的图片才能显示出来。

models.py

  1. # course/models.py
  2. class Course:
  3. ...
  4. def new_brief(self):
  5. data = self.brief
  6. server_addr = contains.SERVER_ADDR
  7. data = data.replace('/media',f'{server_addr}/media')
  8. return data

settings/constants.py

  1. SERVER_ADDR = 'http://www.lyapi.com:8001'

serializers.py

  1. class CourseDetailModelSerializer:
  2. ...
  3. teacher = TeacherModelSerializer()
  4. class Meta:
  5. model = model.Course
  6. fields = [.........,teacher,level_name,new_brief] # 将new_brief添加到字段中

7.表情和图片应用不同的CSS样式

  1. class Course:
  2. ...
  3. def new_brief(self):
  4. data = self.brief
  5. server_addr = contains.SERVER_ADDR
  6. '''
  7. 做了两件事:
  8. 1.将用户上传图片的相对路径改成了绝对路径
  9. 2.让图片和表情应用不同的CSS样式
  10. '''
  11. data = data.replace('src="/media',f'class="img_xx" src="{server_addr}/media')
  12. return data

6.课程章节和课时显示-后端接口

urls.py

  1. re_path(r'chapter/', views.ChapterView.as_view(),),

views.py

  1. from django_filters.rest_framework import DjangoFilterBackend
  2. class ChapterView(ListAPIView):
  3. queryset = models.CourseChapter.objects.filter(is_deleted=False,is_show=True)
  4. serializer_class = CourseChapterModelSerializer
  5. filter_backends = [DjangoFilterBackend,]
  6. filter_fields = ('course',)

serializers.py

  1. class CourseLessonModelSerializer:
  2. class Meta:
  3. model = models.CourseLesson
  4. fields = ['name','section_link','duration','free_trail','lesson']
  5.  
  6. class CourseChapterModelSerialzer:
  7.  
  8. '''在一的序列化器嵌套多的序列化器,切记要加参数many=True'''
  9. coursesection = CourseLessonModelSerializer(many=True) # 1201
  10.  
  11. class Meta:
  12. model = models.CourseChapter
  13. fields = ['chapter','name']

drf测试接口:course/chapter/?course=1

7.课程章节和课时显示-前端

  1. <div class="tab-item" v-if="tabIndex==2">
  2. <div class="tab-item-title">
  3. <p class="chapter">课程章节</p>
  4. <p class="chapter-length">共{{chapter_data.length}}章 </p>
  5. </div>
  6. <div class="chapter-item" v-for="(chapter,chapterindex) in chapter_data">
  7. <p class="chapter-title"><img src="/static/img/1.png" alt="">第{{chapter.chapter}}章·{{chapter.name}}</p>
  8. <ul class="lesson-list">
  9. <li class="lesson-item" v-for="(lesson,lesson_index) in chapter.coursesections">
  10. <p class="name"><span class="index">{{chapter.chapter}}-{{lesson.lesson}}</span> 课程介绍-{{lesson.name}}<span v-show="lesson.free_trail" class="free">免费</span></p>
  11. <p class="time">{{lesson.duration}} <img src="/static/img/chapter-player.svg"></p>
  12. <button class="try" v-if="lesson.free_trail">立即试学</button>
  13. <button class="try" v-else>立即buy</button>
  14. </li>
  15.  
  16. </ul>
  17. </div>
  18.  
  19. </div>

课程章节和课时显示-HTML

  1. // js
  2. export default {
  3. name: "Detail",
  4. data(){
  5. return {
  6.  
  7. chapter_data:{},
  8.  
  9. }
  10.  
  11. },
  12. created(){
  13.  
  14. this.get_chapter_data();
  15. },
  16. methods: {
  17.  
  18. get_chapter_data(){
  19. this.$axios.get(`${this.$settings.Host}/course/chapter/`,{
  20. params:{
  21. course:this.course_id,
  22. }
  23. }).then((res)=>{
  24. console.log(res.data);
  25. this.chapter_data = res.data
  26. })
  27. },
  28.  
  29. }

课程章节和课时显示-JS

day82:luffy:课程详情页面显示&章节和课时显示&视频播放组件&CKEditor富文本编辑器的更多相关文章

  1. 项目页面集成ckeditor富文本编辑器

    步骤一.引入ckeditor.js (注:本实例以ThinkPHP3.2框架为载体,不熟悉ThinkPHP的朋友请自行补习,ckeditor文件代码内容也请去ckeditor官网自行下载) 作为程序员 ...

  2. day81:luffy:课程分类页面&课程信息页面&指定分类显示课程信息&分页显示课程信息

    目录 1.构建课程前端初始页面 2.course后端的准备工作 3.后端实现课程分类列表接口 4.前端发送请求-获取课程分类信息 5.后端实现课程列表信息的接口 6.前端显示列表课程信息 7.按照指定 ...

  3. 前端Vue项目——课程详情页面实现

    一.详情页面路由跳转 应用 Vue Router 编程式导航通过 this.$router.push() 来实现路由跳转. 1.绑定查看详情事件 修改 src/components/Course/Co ...

  4. Django day 33 vue中使用element-ui的使用,课程的相关介绍,vue绑定图片,课程列表接口,课程详情页面

    一:vue中使用element-ui的使用, 二:课程的相关介绍, 三:vue绑定图片, 四:课程列表接口, 五:课程详情页面

  5. Layui之动态循环遍历出的富文本编辑器显示

    这篇记得是工作中的例子 描述: 平常的富文本显示都是根据静态的html获取id来显示,比如: <textarea class="layui-textarea" id=&quo ...

  6. 「newbee-mall新蜂商城开源啦」 页面优化,最新版 wangEditor 富文本编辑器整合案例

    大家比较关心的新蜂商城 Vue3 版本目前已经开发了大部分内容,相信很快就能够开源出来让大家尝鲜了,先让大家看看当前的开发进度: 开源仓库地址为 https://github.com/newbee-l ...

  7. Ueditor富文本编辑器--Ctrl V 粘贴后原有图片显示错误

    最近负责将公司官网从静态网站改版成动态网站,方便公司推广营销人员修改增加文案,避免官网文案维护过于依赖技术人员.在做后台管理系统时用到了富文本编辑器Ueditor,因为公司有一个阿里云文件资源服务器, ...

  8. PHP UEditor富文本编辑器 显示 后端配置项没有正常加载,上传插件不能正常使用

    UEditor是由百度web前端研发部开发所见即所得富文本web编辑器,具有轻量,可定制,注重用户体验等特点,开源基于MIT协议,允许自由使用和修改代码... 问题描述 我的编辑器在本地测试的时候没问 ...

  9. MVC中提交包含HTML代码的页面处理方法(尤其是在使用kindeditor富文本编辑器的时候)

    针对文本框中有HTML代码提交时,mvc的action默认会阻止提交,主要是出于安全考虑.如果有时候需求是要将HTML代码同表单一起提交,那么这时候我们可以采取以下两种办法实现: 1.给Control ...

随机推荐

  1. SpringBoot整合SpringDataJPA,今天没啥事情就看了一下springboot整合springdataJPA,实在是香啊,SQL语句都不用写了

    SpringBoot整合SpringDataJPA 1.JPA概念 JPA是Java Persistence API的简称,中文名Java持久层API,是JDK 5.0注解或XML描述对象-关系表的映 ...

  2. 0921 LCA练习

    1.poj 1330 数据结构中的树,在计算机科学中是非常重要的,例如我们来看看下面这棵树: 在图中我们对每个节点都有编号了. 8号节点是这棵树的根.我们定义,一个子节点向它的根节点的路径上,任意一个 ...

  3. C 多态 RT-Thread

    // RT-Thread对象模型采用结构封装中使用指针的形式达到面向对象中多态的效果,例如: // 抽象父类 #include <stdio.h> #include <assert. ...

  4. 未能找到元素“appender”的架构信息

    在App.config写入log4net的配置信息,然后错误列表就出现了一堆的消息提示. 未能找到元素"appender-ref"的架构信息 未能找到元素"appende ...

  5. Vue学习使用系列九【axiox全局默认配置以及结合Asp.NetCore3.1 WebApi 生成显示Base64的图形验证码】

    1:前端code 1 <!DOCTYPE html> 2 <html lang="en"> 3 4 <head> 5 <meta char ...

  6. Croppie -一个Javascript图像Croppie

    下载 Croppie -一个Javascript图像CroppieCroppie -一个Javascript图像Croppie 安装 凉棚:凉棚安装作物 Npm: Npm安装作物 下载: croppi ...

  7. devops构建IT服务供应链

    1. devops构建IT服务供应链   1) 什么是devops devops是 "开发" 和"运维"的缩写 devops是一组最佳实践强调(IT研发.运维. ...

  8. Android Jetpack从入门到精通(深度好文,值得收藏)

    前言 即学即用Android Jetpack系列Blog的目的是通过学习Android Jetpack完成一个简单的Demo,本文是即学即用Android Jetpack系列Blog的第一篇. 记得去 ...

  9. Java常见的一些经典面试题(附答案解析)

    前言: 我想每个程序员比较头疼的事情都是:工作拧螺丝,面试造火箭吧.但是又必须经历这个过程,尤其是弄不清面试官问的问题,如果你准备的不是很充分,会导致面试的时候手足无措.今天这篇文章是从已工作5年的程 ...

  10. centos8平台使用ulimit做系统资源限制

    一,ulimit的用途 1, ulimit 用于shell启动进程所占用的资源,可用于修改系统资源限制 2, 使用ulimit -a 可以查看当前系统的所有限制值 使用ulimit -n <可以 ...