前台
课程列表页
前端显示课程列表页面
views/Course.vue,代码:
<template> <div class="course"> <Header></Header> <div class="top-wrap"> <div class="actual-header"> <div class="actual-header-wrap"> <div class="banner"> <router-link class="title" to="/course"><img class="h100" src="../assets/coding-title.png" alt=""></router-link> <div>真实项目实战演练</div> </div> <div class="actual-header-search"> <div class="search-inner"> <input class="actual-search-input" placeholder="搜索感兴趣的实战课程内容" type="text" autocomplete="off"> <img class="actual-search-button" src="../assets/search.svg" /> </div> <div class="actual-searchtags"> </div> <div class="search-hot"> <span>热搜:</span> <a href="">Java工程师</a> <a href="">Vue</a> </div> </div> </div> </div> <div class="type"> <div class="type-wrap"> <div class="one warp"> <span class="name">方向:</span> <ul class="items"> <li class="cur"><a href="">全部</a></li> <li><a href="">前端开发</a></li> <li><a href="">后端开发</a></li> <li><a href="">移动开发</a></li> <li><a href="">计算机基础</a></li> <li><a href="">前沿技术</a></li> <li><a href="">云计算&大数据</a></li> <li><a href="">运维&测试</a></li> <li><a href="">数据库</a></li> <li><a href="">UI设计&多媒体</a></li> <li><a href="">游戏</a></li> <li><a href="">求职面试</a></li> </ul> </div> <div class="two warp"> <span class="name">分类:</span> <ul class="items"> <li class="cur"><a href="">不限</a></li> <li><a href="">Vue.js</a></li> <li><a href="">Typescript</a></li> <li><a href="">React.JS</a></li> <li><a href="">HTML/CSS</a></li> <li><a href="">JavaScript</a></li> <li><a href="">Angular</a></li> <li><a href="">Node.js</a></li> <li><a href="">WebApp</a></li> <li><a href="">小程序</a></li> <li><a href="">前端工具</a></li> <li><a href="">CSS</a></li> <li><a href="">Html5</a></li> <li><a href="">CSS3</a></li> </ul> </div> </div> </div> </div> <div class="main"> <div class="main-wrap"> <div class="filter clearfix"> <div class="sort l"> <a href="" class="on">最新</a> <a href="">销量</a> <a href="">升级</a> </div> <div class="other r clearfix"><a class="course-line l" href="" target="_blank">学习路线</a></div> </div> <ul class="course-list clearfix"> <li class="course-card"> <a target="_blank" href=""> <div class="img"><img src="../assets/course-1.png" alt=""></div> <p class="title ellipsis2">全面的Docker 系统性入门+进阶实践(2021最新版)</p> <p class="one"> <span>进阶 · 611人报名</span> <span class="discount r"><i class="name">优惠价</i></span> </p> <p class="two clearfix"> <span class="price l red bold">¥428.00</span> <span class="origin-price l delete-line">¥488.00</span> <span class="add-shop-cart r"><img class="icon imv2-shopping-cart" src="../assets/cart2.svg">加购物车</span> </p> </a> </li> <li class="course-card"> <a target="_blank" href=""> <div class="img"><img src="../assets/course-2.png" alt=""></div> <p class="title ellipsis2">Flink+ClickHouse 玩转企业级实时大数据开发,助你实现弯道超车</p> <p class="one"> <span>进阶 · 246人报名</span> <span class="discount r"><i class="name">限时优惠</i><i class="countdown">6<span class="day">天</span>01:39:21</i></span> </p> <p class="two clearfix"> <span class="price l red bold">¥328.00</span> <span class="origin-price l delete-line">¥368.00</span> <span class="add-shop-cart r"><img class="icon imv2-shopping-cart" src="../assets/cart2.svg">加购物车</span> </p> </a> </li> <li class="course-card"> <a target="_blank" href=""> <div class="img"><img src="../assets/course-3.png" alt=""></div> <p class="title ellipsis2">Flink+ClickHouse 玩转企业级实时大数据开发,助你实现弯道超车</p> <p class="one"> <span>进阶 · 246人报名</span> <span class="discount r"><i class="name">限时优惠</i><i class="countdown">16<span class="day">天</span>01:39:21</i></span> </p> <p class="two clearfix"> <span class="price l red bold">¥328.00</span> <span class="origin-price l delete-line">¥368.00</span> <span class="add-shop-cart r"><img class="icon imv2-shopping-cart" src="../assets/cart2.svg">加购物车</span> </p> </a> </li> <li class="course-card"> <a target="_blank" href=""> <div class="img"><img src="../assets/course-4.png" alt=""></div> <p class="title ellipsis2">Flink+ClickHouse 玩转企业级实时大数据开发,助你实现弯道超车</p> <p class="one"><span>进阶 · 246人报名</span></p> <p class="two clearfix"> <span class="price l red bold">¥399.00</span> <span class="add-shop-cart r"><img class="icon imv2-shopping-cart" src="../assets/cart2.svg">加购物车</span> </p> </a> </li> <li class="course-card"> <a target="_blank" href=""> <div class="img"><img src="../assets/course-5.png" alt=""></div> <p class="title ellipsis2">Flink+ClickHouse 玩转企业级实时大数据开发,助你实现弯道超车</p> <p class="one"><span>进阶 · 246人报名</span></p> <p class="two clearfix"> <span class="price l red bold">¥399.00</span> <span class="add-shop-cart r"><img class="icon imv2-shopping-cart" src="../assets/cart2.svg">加购物车</span> </p> </a> </li> </ul> <div class="page"> <span class="disabled_page">首页</span> <span class="disabled_page">上一页</span> <a href="" class="active">1</a> <a href="">2</a> <a href="">3</a> <a href="">4</a> <a href="">下一页</a> <a href="">尾页</a> </div> </div> </div> <Footer></Footer> </div> </template> <script setup> import {reactive,ref} from "vue" import Header from "../components/Header.vue" import Footer from "../components/Footer.vue" </script> <style scoped> .top-wrap { background-color: #f5f7fa; background-repeat: no-repeat; background-position: top center; background-size: cover } .actual-header{ max-width: 1500px; margin: 0 auto; } .actual-header .actual-header-wrap { height: 100%; display: -webkit-box; display: -ms-flexbox; display: -webkit-flex; display: flex; -webkit-box-align: center; -ms-flex-align: center; -webkit-align-items: center; align-items: center; -webkit-box-pack: justify; -ms-flex-pack: justify; -webkit-justify-content: space-between; justify-content: space-between; padding-top: 8px } .actual-header .actual-header-wrap .banner { display: -webkit-box; display: -ms-flexbox; display: -webkit-flex; display: flex; -webkit-box-align: center; -ms-flex-align: center; -webkit-align-items: center; align-items: center } .actual-header .actual-header-wrap .banner .title { height: 46px; margin-right: 8px } .actual-header .actual-header-wrap .actual-header-search { position: relative; width: 320px } .actual-header .actual-header-wrap .actual-header-search .search-inner { width: 100%; border-radius: 4px; overflow: hidden; margin: 17px 0 7px; border: 1px solid rgba(84,92,99,.2) } .actual-header .actual-header-wrap .actual-header-search .search-inner .actual-search-input { width: 275px; font-size: 12px; color: #93999f; line-height: 24px; padding: 5px 12px; border: none; border-radius: 0; box-sizing: border-box; background: 0 0 } .actual-header .actual-header-wrap .actual-header-search .search-inner .actual-search-input::-webkit-input-placeholder { color: #9199a1 } .actual-header .actual-header-wrap .actual-header-search .search-inner .actual-search-input::-moz-placeholder { color: #9199a1 } .actual-header .actual-header-wrap .actual-header-search .search-inner .actual-search-input:-moz-placeholder { color: #9199a1 } .actual-header .actual-header-wrap .actual-header-search .search-inner .actual-search-input:-ms-input-placeholder { color: #9199a1 } .actual-header .actual-header-wrap .actual-header-search .search-inner .actual-search-button { width: 26px; padding-top: 4px; padding-bottom: 4px; padding-right: 4px; padding-left: 6px; height: 26px; font-size: 18px; text-align: center; line-height: 26px; color: #fff; background-color: rgba(84,92,99,.2); cursor: pointer; border-top-right-radius: 4px; border-bottom-right-radius: 4px; float: right } .actual-header .actual-header-wrap .actual-header-search .actual-searchtags { position: absolute; right: 128px; top: 0; height: 48px; line-height: 48px; text-align: right } .actual-header .actual-header-wrap .actual-header-search .actual-searchtags a { margin-left: 24px; font-size: 12px; color: #4d555d; line-height: 48px } .actual-header .actual-header-wrap .actual-header-search .actual-searchtags a:hover { color: #f01414 } .actual-header .actual-header-wrap .actual-header-search .actual-history-item a { float: left; font-size: 12px; color: rgba(7,17,27,.6); line-height: 16px; padding: 4px 12px; margin-right: 8px; background: rgba(7,17,27,.05); border-radius: 12px; transition: .3s background,color linear; margin-top: 8px } .actual-header .actual-header-wrap .actual-header-search .actual-history-item a:hover { background: rgba(7,17,27,.1); color: #07111b } .actual-header .actual-header-wrap .actual-header-search li { display: block; width: 100%; height: 48px; transition: .3s background linear; padding: 12px 16px; box-sizing: border-box; font-size: 14px; color: #4d555d; line-height: 24px; cursor: pointer; z-index: 1 } .actual-header .actual-header-wrap .actual-header-search li:hover { background: #f3f5f7; color: #07111b } .actual-header .actual-header-wrap .actual-header-search .search-hot { height: 21px; overflow: hidden; padding-left: 14px } .actual-header .actual-header-wrap .actual-header-search .search-hot a, .actual-header .actual-header-wrap .actual-header-search .search-hot span { color: rgba(84,92,99,.7); font-size: 12px; line-height: 16px } .actual-header .actual-header-wrap .actual-header-search .search-hot a { margin-right: 14px } .actual-header .actual-header-wrap .actual-header-search .search-hot a:last-child { margin-right: 0 } .type { max-width: 1500px; margin: 0 auto; padding-bottom: 27px } .type .type-wrap { position: relative; height: 109px } .type .type-wrap .warp { display: -webkit-box; display: -ms-flexbox; display: -webkit-flex; display: flex; position: absolute; width: 1430px; height: 54px; overflow: hidden; padding: 10px; box-sizing: border-box; box-shadow: 0 12px 20px 0 rgba(95,101,105,0); border-radius: 8px; transition: all .2s } .type .type-wrap .warp.one { margin-bottom: 25px; z-index: 3 } .type .type-wrap .warp.two { top: 59px; z-index: 2 } .type .type-wrap .warp .name { width: 3em; color: #07111b; line-height: 32px; font-weight: 700; margin-right: 6px } .type .type-wrap .warp .items { width: 0; -webkit-box-flex: 1; -ms-flex: 1; -webkit-flex: 1; flex: 1 } .type .type-wrap .warp .items li { float: left; line-height: 16px; padding: 8px; border-radius: 6px; margin: 0 12px 12px 0 } .type .type-wrap .warp .items li a { color: #1c1f21 } .type .type-wrap .warp .items li.cur { background-color: rgba(233,142,70,.1) } .type .type-wrap .warp .items li.cur a { color: #e98e46 } .delete-line{ text-decoration: line-through; } /******** 课程列表 ********/ .l{ float: left; } .r{ float: right; } .red{ color: red; } .bold{ font-weight: 700; } .main { margin-bottom: 60px } .main .main-wrap{ max-width: 1500px; margin: 0 auto; } .clearfix:after { content: ''; display: block; height: 0; clear: both; visibility: hidden } .main .filter { margin: 20px 0 } .main .filter .sort { overflow: hidden } .main .filter .sort a { display: inline-block; float: left; font-size: 12px; color: #545c63; line-height: 16px; padding: 4px 12px; border-radius: 100px; margin-right: 12px } .main .filter .sort a:last-child { margin-right: 0 } .main .filter .sort a.on { color: #fff; background-color: #545c63 } .main .filter .other { font-size: 12px } .main .filter .other .course-line { color: #e98e46; line-height: 16px; padding: 4px 16px; border-radius: 100px; background-color: rgba(233,142,70,.1); margin-left: 24px } .main .course-list .course-card { position: relative; width: 270px; height: 270px; float: left; margin: 0 37px 20px 0; box-shadow: 0 4px 8px 0 rgba(95,101,105,.05); border-radius: 8px; background-color: #fff; transition: transform .2s,box-shadow .2s } .main .course-list .course-card:nth-child(5n) { margin-right: 0 } .main .course-list .course-card:hover { transform: translateY(-2px); box-shadow: 0 12px 20px 0 rgba(95,101,105,.1) } .main .course-list .course-card a { display: inline-block; width: 100% } .main .course-list .course-card .img { height: 152px; background: no-repeat center/cover; margin-bottom: 8px; border-radius: 8px 8px 0 0; overflow: hidden } .main .course-list .course-card .title { color: #545c63; line-height: 20px; height: 40px; margin-bottom: 8px; padding: 0 8px } .main .course-list .course-card .title.ellipsis2 { overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical } .main .course-list .course-card .one, .main .course-list .course-card .two { font-size: 12px; color: #9199a1; line-height: 18px; padding: 0 8px; margin-bottom: 8px } .main .course-list .course-card .one .add-shop-cart .icon, .main .course-list .course-card .one .star .icon, .main .course-list .course-card .two .add-shop-cart .icon, .main .course-list .course-card .two .star .icon { display: inline-block; margin-right: 2px; font-size: 14px } .imv2-shopping-cart{ width: 14px; } .main .course-list .course-card .one .add-shop-cart.add-shop-cart, .main .course-list .course-card .one .add-shop-cart.stared, .main .course-list .course-card .one .star.add-shop-cart, .main .course-list .course-card .one .star.stared, .main .course-list .course-card .two .add-shop-cart.add-shop-cart, .main .course-list .course-card .two .add-shop-cart.stared, .main .course-list .course-card .two .star.add-shop-cart, .main .course-list .course-card .two .star.stared { color: #ff655d } .main .course-list .course-card .one .discount i, .main .course-list .course-card .two .discount i { font-style: normal; padding: 3px 4px } .main .course-list .course-card .one .discount i.name, .main .course-list .course-card .two .discount i.name { color: #fff; background-color: rgba(242,13,13,.6) } .main .course-list .course-card .one .price, .main .course-list .course-card .two .price { line-height: 20px; margin-right: 2px } .main .course-list .course-card .one .discount, .main .course-list .course-card .two .discount { border: 1px solid rgba(242,13,13,.2); border-radius: 2px; font-size: 12px; line-height: 1; margin-right: 4px; overflow: hidden; display: -webkit-box; display: -ms-flexbox; display: -webkit-flex; display: flex; -webkit-box-align: center; -ms-flex-align: center; -webkit-align-items: center; align-items: center } .main .course-list .course-card .one .discount i, .main .course-list .course-card .two .discount i { font-style: normal; padding: 3px 4px } .main .course-list .course-card .one .discount i.name, .main .course-list .course-card .two .discount i.name { color: #fff; background-color: rgba(242,13,13,.6) } .main .course-list .course-card .one .discount i.countdown, .main .course-list .course-card .two .discount i.countdown { display: flex; font-family: DINCondensed,'微软雅黑'; color: #f76e6e; padding-top: 4px; padding-bottom: 2px } .main .course-list .course-card .one .discount i.countdown .day, .main .course-list .course-card .two .discount i.countdown .day { display: inline-block; width: 12px; height: 12px; transform:scale(0.8); } /**** 页码 *****/ .page { margin: 25px 0 auto; overflow: hidden; clear: both; text-align: center } .page a { display: inline-block; margin: 0 12px; width: 36px; height: 36px; line-height: 36px; font-size: 14px; color: #4d555d; text-align: center; border-radius: 50%; -webkit-transition: border-color .2s; -moz-transition: border-color .2s; transition: border-color .2s } .page a:hover { text-decoration: none; background-color: #d9dde1 } .page a.active { background: #4d555d; color: #fff } .page a:first-child, .page a:last-child, .page a:nth-child(2), .page a:nth-last-child(2) { width: auto } .page a:first-child:hover, .page a:last-child:hover, .page a:nth-child(2):hover, .page a:nth-last-child(2):hover { background-color: transparent } .page span { display: inline-block; padding: 0 12px; min-width: 20px; height: 39px; line-height: 39px; font-size: 14px; color: #93999f; text-align: center } </style>View Code
注册路由,src/router/index.js,代码:
import {createRouter, createWebHistory, createWebHashHistory} from 'vue-router' import store from "../store"; // 路由列表 const routes = [ { meta:{ title: "luffy2.0-站点首页", keepAlive: true }, path: '/', // uri访问地址 name: "Home", component: ()=> import("../views/Home.vue") }, { meta:{ title: "luffy2.0-用户登录", keepAlive: true }, path:'/login', // uri访问地址 name: "Login", component: ()=> import("../views/Login.vue") }, { meta:{ title: "luffy2.0-用户注册", keepAlive: true }, path: '/register', name: "Register", // 路由名称 component: ()=> import("../views/Register.vue"), // uri绑定的组件页面 }, { meta:{ title: "luffy2.0-个人中心", keepAlive: true, authorization: true, }, path: '/user', name: "User", component: ()=> import("../views/User.vue"), }, { meta:{ title: "luffy2.0-课程列表", keepAlive: true, }, path: '/project', name: "Course", component: ()=> import("../views/Course.vue"), }, ] // 路由对象实例化 const router = createRouter({ // history, 指定路由的模式 history: createWebHistory(), // 路由列表 routes, }); // 导航守卫 router.beforeEach((to, from, next)=>{ document.title=to.meta.title // 登录状态验证 if (to.meta.authorization && !store.getters.getUserInfo) { next({"name": "Login"}) }else{ next() } }) // 暴露路由对象 export default routerView Code
将素材图片复制到src/asserts文件夹中。
课程功能管理的设计
分析课程列表页面中的出现的数据之间的关系
学习方向:
课程分类:
课程信息:
课程章节:
课时信息:
老师信息:
价格策略:(限时免费\限时折扣\限时满减\原价)
优惠券/积分:
E-R图
此工具绘制: http://draw.io
E-R图描述的是数据库设计过程中,实体(表名)与实体之间的关系的,实体与属性(字段)之间的关联的。
矩形表示实体(表),所谓的实体就是可以相互区分的,独立的事物。实体在数据库中会被转换成数据表。
椭圆形表示属性(字段),用于描述实体的特征。实体的属性在数据库中会被转换成数据表中的字段。
菱形则表示实体之间的关系,根据范式理论第三条,实体之间的关系存在如下:
1:N 1对多
1:1 1对1
N:M 多对多
UML图
物理模型(根据具体数据库来设计的,powerdesigner、navicat)
合并分支打标签
# 确认前面功能已经开发完整,review代码结束,向公司申请合并分支,开发合并分支 cd /home/moluo/Desktop/luffycity git add . git commit -m "feature: 展示课程列表页" git push # 切换分支 git checkout master # 合并代码操作 git merge feature/user # 删除分支 git branch -d feature/user # 查看线上本地所有的分支列表,可以看到本地的feature/user分支已经删除,但是线上的依然存在。 git branch --all # 本地删除了分支以后,线上分支也要同步一下。 git push origin --delete feature/user # 因为属于一个较大功能的开发合并,往往项目中都会打一个标签 git tag v0.0.3 # 提交标签版本 git push --tag # git push origin v0.0.3
合并代码冲突:(删掉冲突文件 这几个都是些缓存)
下面根据ER图创建数据库:
逆范式(违背了范式的理论)(空间换时间):
为了让查询数据的速度的提高,添加一些必要的冗余字段。
本质:以空间(廉价的硬盘空间)换时间(宝贵的查询时间)
三范式:
1NF;不可分割,是指数据库的每一列都是不可分割的基本数据项,同一列不能有多个值,即实体中的某个属性不能有多个值或者不能有重复的属性。 2NF:不可重复,该数据表中的任何一个非主键字段的数值都依赖于该数据表的主键字段 3NF:不能冗余,消除非主属性对主关系键的传递函数依赖
1:1 一对一 1:n 一对多 m:n 多对多
课程子应用创建
# 创建新分支 git checkout -b feature/course cd /home/moluo/Desktop/luffycity/luffycityapi/luffycityapi/apps/ # 子应用创建 python ../../manage.py startapp courses
注册子应用
settings/dev,代码:
INSTALLED_APPS = [ ... 'courses', ]View Code
注册子路由,course/urls,代码:
from django.urls import path,re_path from . import views urlpatterns = [ ]View Code
luffycityapi/urls,代码:
from django.contrib import admin from django.urls import path,re_path,include from django.conf import settings from django.views.static import serve # 静态文件代理访问模块 urlpatterns = [ path('admin/', admin.site.urls), re_path(r'uploads/(?P<path>.*)', serve, {"document_root": settings.MEDIA_ROOT}), path("", include("home.urls")), path("users/", include("users.urls")), path("courses/", include("courses.urls")), ]View Code
数据模型创建
courses/models.py,代码:
from luffycityapi.utils.models import models,BaseModel # Create your models here. class CourseDirection(BaseModel): name = models.CharField(max_length=255, unique=True, verbose_name="方向名称") remark = models.TextField(default="", blank=True, null=True, verbose_name="方向描述") recomment_home_hot = models.BooleanField(default=False, verbose_name="是否推荐到首页新课栏目") recomment_home_top = models.BooleanField(default=False, verbose_name="是否推荐到首页必学栏目") class Meta: db_table = "fg_course_direction" verbose_name = "学习方向" verbose_name_plural = verbose_name def __str__(self): return self.name class CourseCategory(BaseModel): name = models.CharField(max_length=255, unique=True, verbose_name="分类名称") remark = models.TextField(default="", blank=True, null=True, verbose_name="分类描述") # related_name反向引用属性名 # 数据库外键设为虚拟外键 db_constraint=False(课程分类和学习方向两张表就没什么绑定关系了) direction = models.ForeignKey("CourseDirection", related_name="category_list", on_delete=models.DO_NOTHING, db_constraint=False, verbose_name="学习方向") class Meta: db_table = "fg_course_category" verbose_name = "课程分类" verbose_name_plural = verbose_name def __str__(self): return self.name class Course(BaseModel): course_type = ( (0, '付费购买'), (1, '会员专享'), (2, '学位课程'), ) level_choices = ( (0, '初级'), (1, '中级'), (2, '高级'), ) status_choices = ( (0, '上线'), (1, '下线'), (2, '预上线'), ) course_cover = models.ImageField(upload_to="course/cover", max_length=255, verbose_name="封面图片", blank=True, null=True) course_video = models.FileField(upload_to="course/video", max_length=255, verbose_name="封面视频", blank=True, null=True) course_type = models.SmallIntegerField(choices=course_type,default=0, verbose_name="付费类型") level = models.SmallIntegerField(choices=level_choices, default=1, verbose_name="难度等级") description = models.TextField(null=True, blank=True, verbose_name="详情介绍") pub_date = models.DateField(auto_now_add=True, verbose_name="发布日期") period = models.IntegerField(default=7, verbose_name="建议学习周期(day)") attachment_path = models.FileField(max_length=1000, blank=True, null=True, verbose_name="课件路径") attachment_link = models.CharField(max_length=1000, blank=True, null=True, verbose_name="课件链接") status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="课程状态") students = models.IntegerField(default=0, verbose_name="学习人数") lessons = models.IntegerField(default=0, verbose_name="总课时数量") pub_lessons = models.IntegerField(default=0, verbose_name="已更新课时数量") price = models.DecimalField(max_digits=10,decimal_places=2, verbose_name="课程原价",default=0) recomment_home_hot = models.BooleanField(default=False, verbose_name="是否推荐到首页新课栏目") recomment_home_top = models.BooleanField(default=False, verbose_name="是否推荐到首页必学栏目") direction = models.ForeignKey("CourseDirection", related_name="course_list", on_delete=models.DO_NOTHING, null=True, blank=True, db_constraint=False, verbose_name="学习方向") category = models.ForeignKey("CourseCategory", related_name="course_list", on_delete=models.DO_NOTHING, null=True, blank=True, db_constraint=False, verbose_name="课程分类") teacher = models.ForeignKey("Teacher", related_name="course_list", on_delete=models.DO_NOTHING, null=True, blank=True, db_constraint=False, verbose_name="授课老师") class Meta: db_table = "fg_course_info" verbose_name = "课程信息" verbose_name_plural = verbose_name def __str__(self): return "%s" % self.name class Teacher(BaseModel): role_choices = ( (0, '讲师'), (1, '导师'), (2, '班主任'), ) role = models.SmallIntegerField(choices=role_choices, default=0, verbose_name="讲师身份") title = models.CharField(max_length=64, verbose_name="职位、职称") signature = models.CharField(max_length=255, blank=True, null=True, verbose_name="导师签名") avatar = models.ImageField(upload_to="teacher", null=True, verbose_name="讲师头像") brief = models.TextField(max_length=1024, verbose_name="讲师描述") class Meta: db_table = "fg_teacher" verbose_name = "讲师信息" verbose_name_plural = verbose_name def __str__(self): return "%s" % self.name class CourseChapter(BaseModel): """课程章节""" orders = models.SmallIntegerField(default=1, verbose_name="第几章") summary = models.TextField(blank=True, null=True, verbose_name="章节介绍") pub_date = models.DateField(auto_now_add=True, verbose_name="发布日期") course = models.ForeignKey("Course", related_name='chapter_list', on_delete=models.CASCADE, db_constraint=False, verbose_name="课程名称") class Meta: db_table = "fg_course_chapter" verbose_name = "课程章节" verbose_name_plural = verbose_name def __str__(self): return "%s-第%s章-%s" % (self.course.name, self.orders, self.name) class CourseLesson(BaseModel): """课程课时""" lesson_type_choices = ( (0, '文档'), (1, '练习'), (2, '视频'), ) orders = models.SmallIntegerField(default=1, verbose_name="第几节") lesson_type = models.SmallIntegerField(default=2, choices=lesson_type_choices, verbose_name="课时种类") lesson_link = models.CharField(max_length=255, blank=True, null=True, help_text="若是video,填视频地址或者视频id,若是文档,填文档地址", verbose_name="课时链接") duration = models.CharField(blank=True, null=True, max_length=32, verbose_name="课时时长") # 仅在前端展示使用 pub_date = models.DateTimeField(auto_now_add=True, verbose_name="发布时间") free_trail = models.BooleanField(default=False, verbose_name="是否可试看") recomment = models.BooleanField(default=False, verbose_name="是否推荐到课程列表") chapter = models.ForeignKey("CourseChapter", related_name='lesson_list', on_delete=models.CASCADE, db_constraint=False, verbose_name="章节") course = models.ForeignKey("Course", related_name="lesson_list", on_delete=models.DO_NOTHING, db_constraint=False, verbose_name="课程") class Meta: db_table = "fg_course_lesson" verbose_name = "课程课时" verbose_name_plural = verbose_name def __str__(self): return "%s-%s" % (self.chapter, self.name)View Code
执行数据迁移
cd /home/moluo/Desktop/luffycity/luffycityapi python manage.py makemigrations python manage.py migrate
因为我们一次性创建6个数据表,所以我们需要提供一个后台运营站点给将来工作人员添加对应的数据。当然我们现在也要添加数据,所以这里,我们采用simpleui来美化django内置的admin站点。
提交版本
cd /home/moluo/Desktop/luffycity git add . git commit -m "feature: 课程子应用与课程相关模型的创建" git push
Admin
simpleui美化admin站点(后台)
官网:https://simpleui.72wo.com/simpleui/
simpleui 免费版本
simplePro 收费版本
安装simpleui
pip install django-simpleui
注册simpleui,settings/dev.py,代码:
INSTALLED_APPS = [ 'simpleui', # admin界面美化,必须写在admin上面 'django.contrib.admin', # 内置的admin运营站点 # ... ]View Code
settings/dev(修改):(admin站点内的转换成汉语显示)
LANGUAGE_CDDE = "zh-hans" TIME_ZONE = "Asia/Shanghai"View Code
把当前新增的课程的相关模型注册到admin里面.simpleUI仅仅是修改了admin站点的外观效果以及新增了部分配置功能,原有的admin站点的所有功能,simpleUI都没有进行改动或者删减。下面是对admin改造和simpleUI没关系
courses/apps.py,代码:
from django.apps import AppConfig class CoursesConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'courses' verbose_name="课程管理" verbose_name_plural = verbose_nameView Code
users/apps.py,代码:
from django.apps import AppConfig class UsersConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'users' verbose_name="用户管理" verbose_name_plural = verbose_name courses/admin.py,代码: from django.contrib import admin from .models import CourseDirection, CourseCategory, Course, Teacher, CourseChapter, CourseLesson # Register your models here. class CourseDirectionModelAdmin(admin.ModelAdmin): """学习方向的模型管理器""" pass admin.site.register(CourseDirection, CourseDirectionModelAdmin) class CourseCategoryModelAdmin(admin.ModelAdmin): """课程分类的模型管理器""" pass admin.site.register(CourseCategory, CourseCategoryModelAdmin) class CourseModelAdmin(admin.ModelAdmin): """课程信息的模型管理器""" pass admin.site.register(Course, CourseModelAdmin) class TeacherModelAdmin(admin.ModelAdmin): """讲师信息的模型管理器""" pass admin.site.register(Teacher, TeacherModelAdmin) class CourseChapterModelAdmin(admin.ModelAdmin): """课程章节的模型管理器""" pass admin.site.register(CourseChapter, CourseChapterModelAdmin) class CourseLessonModelAdmin(admin.ModelAdmin): """课程课时的模型管理器""" pass admin.site.register(CourseLesson, CourseLessonModelAdmin)View Code
users/admin.py,代码:
from django.contrib import admin from django.contrib.auth.admin import UserAdmin from .models import User # Register your models here. class UserModelAdmin(UserAdmin): pass admin.site.register(User, UserModelAdmin)View Code
给admin后台站点添加富文本编辑器
所谓的富文本编辑器,实际上就是前端javasctript实现的页面插件,这个插件允许我们替换多行文本框,让使用者可以在不懂html、css的情况下,也能像使用word编写文章那样,对页面的部分内容进行图文排版。
常见的富文本编辑器插件:
ckeditor:https://ckeditor.com/ckeditor-5/demo/
kindeditor:http://kindeditor.net/demo.php
我们在django中一般使用就是ckeditor编辑器,可以通过pip安装。
django-ckeditor:https://github.com/django-ckeditor/django-ckeditor
pip install django-ckeditor
注册到项目中,settings/dev.py,代码:
# ckeditor富文本编辑器配置 INSTALLED_APPS = [ 'simpleui', # admin界面美化,必须写在admin上面 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', "rest_framework", # 注意:记得加入 rest_framework 'corsheaders', # cors跨域子应用 'ckeditor', # 富文本编辑器 'ckeditor_uploader',# 富文本编辑器文件上传子应用 "home", 'users', 'courses', ] # 上传文件的存储路径 CKEDITOR_UPLOAD_PATH = "ckeditor/" # 工具条配置 CKEDITOR_CONFIGS = { 'default': { # 'toolbar': 'full', # full 显示全部工具 # 'toolbar': 'Basic', # Basic 显示基本工具 'toolbar': 'Custom', # 自定义工具条的显示数量 'toolbar_Custom': [ ['Bold', 'Italic', 'Underline', 'Image', 'Styles', 'Format', 'Font', 'Fontsize'], ['NumberedList', 'BulletedList', '-', 'Outdent', 'Indent', '-', 'JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock'], ['Link', 'Unlink', 'Table'], ['RemoveFormat', 'Source'] ], # 设置编辑器的高度 'height': 120, }, }View Code
自定义工具条的名称:
总路由,luffycityapi.urls,代码:
urlpatterns = [ # ... path('ckeditor/', include('ckeditor_uploader.urls')), # ... ]View Code
经过上面的配置以后,我们就可以让admin站点在显示模型管理器中的字段时,把models.TextField
字段换成富文本字段。
ckeditor安装成功可以允许开发者在模型中设置2个富文本字段。
# 不支持上传文件 RichTextField from ckeditor.fields import RichTextField # 支持上传文件 RichTextUploadingField from ckeditor_uploader.fields import RichTextUploadingField # 原来的models.TextField字段中所有的设置信息全部不需要改动,因为上面这2个字段都是models.TextField的子类。 所以我们现在可以把课程相关模型所有的models.TextField字段替换成富文本字段了(alt+z 全选要修改的数据)。 courses/models,代码: from luffycityapi.utils.models import models, BaseModel from ckeditor.fields import RichTextField # 不支持上传文件 from ckeditor_uploader.fields import RichTextUploadingField # 支持上传文件 # Create your models here. class CourseDirection(BaseModel): name = models.CharField(max_length=255, unique=True, verbose_name="方向名称") remark = RichTextField(default="", blank=True, null=True, verbose_name="方向描述") recomment_home_hot = models.BooleanField(default=False, verbose_name="是否推荐到首页新课栏目") recomment_home_top = models.BooleanField(default=False, verbose_name="是否推荐到首页必学栏目") class Meta: db_table = "fg_course_direction" verbose_name = "学习方向" verbose_name_plural = verbose_name class CourseCategory(BaseModel): name = models.CharField(max_length=255, unique=True, verbose_name="分类名称") remark = RichTextField(default="", blank=True, null=True, verbose_name="分类描述") direction = models.ForeignKey("CourseDirection", related_name="category_list", db_constraint=False, on_delete=models.DO_NOTHING, verbose_name="学习方向") class Meta: db_table = "fg_course_category" verbose_name = "课程分类" verbose_name_plural = verbose_name class Course(BaseModel): course_type = ( (0, '付费购买'), (1, '会员专享'), (2, '学位课程'), ) level_choices = ( (0, '初级'), (1, '中级'), (2, '高级'), ) status_choices = ( (0, '上线'), (1, '下线'), (2, '预上线'), ) course_cover = models.ImageField(upload_to="course/cover", max_length=255, verbose_name="封面图片", blank=True, null=True) course_video = models.FileField(upload_to="course/video", max_length=255, verbose_name="封面视频", blank=True, null=True) course_type = models.SmallIntegerField(choices=course_type,default=0, verbose_name="付费类型") level = models.SmallIntegerField(choices=level_choices, default=1, verbose_name="难度等级") description = RichTextUploadingField(null=True, blank=True, verbose_name="详情介绍") pub_date = models.DateField(auto_now_add=True, verbose_name="发布日期") period = models.IntegerField(default=7, verbose_name="建议学习周期(day)") attachment_path = models.FileField(max_length=1000, blank=True, null=True, verbose_name="课件路径") attachment_link = models.CharField(max_length=1000, blank=True, null=True, verbose_name="课件链接") status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="课程状态") students = models.IntegerField(default=0, verbose_name="学习人数") lessons = models.IntegerField(default=0, verbose_name="总课时数量") pub_lessons = models.IntegerField(default=0, verbose_name="已更新课时数量") price = models.DecimalField(max_digits=10,decimal_places=2, verbose_name="课程原价",default=0) recomment_home_hot = models.BooleanField(default=False, verbose_name="是否推荐到首页新课栏目") recomment_home_top = models.BooleanField(default=False, verbose_name="是否推荐到首页必学栏目") direction = models.ForeignKey("CourseDirection", related_name="course_list", on_delete=models.DO_NOTHING, null=True, blank=True, db_constraint=False, verbose_name="学习方向") category = models.ForeignKey("CourseCategory", related_name="course_list", on_delete=models.DO_NOTHING, null=True, blank=True, db_constraint=False, verbose_name="课程分类") teacher = models.ForeignKey("Teacher", related_name="course_list", on_delete=models.DO_NOTHING, null=True, blank=True, db_constraint=False, verbose_name="授课老师") class Meta: db_table = "fg_course_info" verbose_name = "课程信息" verbose_name_plural = verbose_name class Teacher(BaseModel): role_choices = ( (0, '讲师'), (1, '导师'), (2, '班主任'), ) role = models.SmallIntegerField(choices=role_choices, default=0, verbose_name="讲师身份") title = models.CharField(max_length=64, verbose_name="职位、职称") signature = models.CharField(max_length=255, blank=True, null=True, verbose_name="导师签名") avatar = models.ImageField(upload_to="teacher", null=True, verbose_name="讲师头像") brief = RichTextUploadingField(max_length=1024, verbose_name="讲师描述") class Meta: db_table = "fg_teacher" verbose_name = "讲师信息" verbose_name_plural = verbose_name class CourseChapter(BaseModel): """课程章节""" orders = models.SmallIntegerField(default=1, verbose_name="第几章") summary = RichTextUploadingField(blank=True, null=True, verbose_name="章节介绍") pub_date = models.DateField(auto_now_add=True, verbose_name="发布日期") course = models.ForeignKey("Course", related_name='chapter_list', on_delete=models.CASCADE, db_constraint=False, verbose_name="课程名称") class Meta: db_table = "fg_course_chapter" verbose_name = "课程章节" verbose_name_plural = verbose_name def __str__(self): return "%s-第%s章-%s" % (self.course.name, self.orders, self.name) class CourseLesson(BaseModel): """课程课时""" lesson_type_choices = ( (0, '文档'), (1, '练习'), (2, '视频'), ) orders = models.SmallIntegerField(default=1, verbose_name="第几课时") lesson_type = models.SmallIntegerField(default=2, choices=lesson_type_choices, verbose_name="课时种类") lesson_link = models.CharField(max_length=255, blank=True, null=True, help_text="若是video,填视频地址或者视频id,若是文档,填文档地址", verbose_name="课时链接") duration = models.CharField(blank=True, null=True, max_length=32, verbose_name="课时时长") # 仅在前端展示使用 pub_date = models.DateTimeField(auto_now_add=True, verbose_name="发布时间") free_trail = models.BooleanField(default=False, verbose_name="是否可试看") recomment = models.BooleanField(default=False, verbose_name="是否推荐到课程列表") chapter = models.ForeignKey("CourseChapter", related_name='lesson_list', on_delete=models.CASCADE, db_constraint=False, verbose_name="章节") course = models.ForeignKey("Course", related_name="lesson_list", on_delete=models.DO_NOTHING, db_constraint=False, verbose_name="课程") class Meta: db_table = "fg_course_lesson" verbose_name = "课程课时" verbose_name_plural = verbose_name def __str__(self): return "%s-第%s章-%s-第%s课时-%s" % (self.course.name,self.chapter.orders, self.chapter.name, self.orders, self.name)View Code
因为这个字段属于ckeditor修改前端外观的而已,所以对于ORM而言,本质上来说还是models.TextField字段,所以不用执行数据迁移。
课程相关模型的admin站点配置在列表页中展示模型字段,
courses/admin.py,代码:
from django.contrib import admin from .models import CourseDirection, CourseCategory, Course, Teacher, CourseChapter, CourseLesson class CourseCategoryInLine(admin.StackedInline): """课程分类的内嵌类""" model = CourseCategory fields = ["id","name","orders"] class CourseDirectionModelAdmin(admin.ModelAdmin): """学习方向的模型管理器""" list_display = ["id","name","recomment_home_hot","recomment_home_top"] # 默认排序字段 ordering = ["id"] # 字段过滤 list_filter = ["recomment_home_hot", "recomment_home_top"] # 搜索字段 search_fields = ["name"] # 内嵌外键数据 inlines = [CourseCategoryInLine, ] # 分页配置,一页数据量 list_per_page = 10 admin.site.register(CourseDirection, CourseDirectionModelAdmin) class CourseCategoryModelAdmin(admin.ModelAdmin): """课程分类的模型管理器""" list_display = ["id","name","direction"] ordering = ["id"] list_filter = ["direction"] search_fields = ["name"] # 分页配置,一页数据量 list_per_page = 10 # 更新数据时的表单配置项 fieldsets = ( ("必填", {'fields': ('name','direction', 'remark')}), ("选填", { 'classes': ('collapse',), 'fields': ('is_show', 'orders'), }), ) # 添加数据时的表单配置项 add_fieldsets = ( (None, { 'classes': ('wide',), 'fields': ('name', 'direction', 'remark'), }), ) # 当前方法会在显示表单的时候,自动执行,返回值就是表单配置项 def get_fieldsets(self, request, obj=None): """ 获取表单配置项 :param request: 客户端的http请求对象 :param obj: 本次修改的模型对象,如果是添加数据操作,则obj为None :return: """ if not obj: return self.add_fieldsets return super().get_fieldsets(request, obj) admin.site.register(CourseCategory, CourseCategoryModelAdmin) class CourseModelAdmin(admin.ModelAdmin): """课程信息的模型管理器""" list_display = ["id","name",'course_cover',"course_type","level","pub_date","students","lessons","price"] # 分页配置,一夜数据量 list_per_page = 10 admin.site.register(Course, CourseModelAdmin) class TeacherModelAdmin(admin.ModelAdmin): """讲师信息的模型管理器""" list_display = ["id","name","avatar","title","role","signature"] # 分页配置,一夜数据量 list_per_page = 10 # 搜索字段 search_fields = ["name", "title", "role", "signature"] admin.site.register(Teacher, TeacherModelAdmin) class CourseChapterModelAdmin(admin.ModelAdmin): """课程章节的模型管理器""" list_display = ["id","text", "pub_date",] # 分页配置,一夜数据量 list_per_page = 10 admin.site.register(CourseChapter, CourseChapterModelAdmin) class CourseLessonModelAdmin(admin.ModelAdmin): """课程课时的模型管理器""" list_display = ["id","text", "text2", "lesson_type", "duration", "pub_date", "free_trail"] # 分页配置,一夜数据量 list_per_page = 10 # 下面是旧版本写法,django2.0版本 -> django3.0以后,建议在模型中声明自定义字段 ---> text2属于新版本写法 def text(self, obj): return obj.__str__() text.admin_order_field = "orders" text.short_description = "课时名称" admin.site.register(CourseLesson, CourseLessonModelAdmin)View Code
courses/modes.py,代码:
class CourseChapter(BaseModel): """课程章节""" orders = models.SmallIntegerField(default=1, verbose_name="第几章") summary = RichTextUploadingField(blank=True, null=True, verbose_name="章节介绍") pub_date = models.DateField(auto_now_add=True, verbose_name="发布日期") course = models.ForeignKey("Course", related_name='chapter_list', on_delete=models.CASCADE, db_constraint=False, verbose_name="课程名称") class Meta: db_table = "fg_course_chapter" verbose_name = "课程章节" verbose_name_plural = verbose_name def __str__(self): return "%s-第%s章-%s" % (self.course.name, self.orders, self.name) # 自定义字段 def text(self): return self.__str__() # admin站点配置排序规则和显示的字段文本提示 text.short_description = "章节名称" text.allow_tags = True text.admin_order_field = "orders" class CourseLesson(BaseModel): """课程课时""" lesson_type_choices = ( (0, '文档'), (1, '练习'), (2, '视频'), ) orders = models.SmallIntegerField(default=1, verbose_name="第几课时") lesson_type = models.SmallIntegerField(default=2, choices=lesson_type_choices, verbose_name="课时种类") lesson_link = models.CharField(max_length=255, blank=True, null=True, help_text="若是video,填视频地址或者视频id,若是文档,填文档地址", verbose_name="课时链接") duration = models.CharField(blank=True, null=True, max_length=32, verbose_name="课时时长") # 仅在前端展示使用 pub_date = models.DateTimeField(auto_now_add=True, verbose_name="发布时间") free_trail = models.BooleanField(default=False, verbose_name="是否可试看") recomment = models.BooleanField(default=False, verbose_name="是否推荐到课程列表") chapter = models.ForeignKey("CourseChapter", related_name='lesson_list', on_delete=models.CASCADE, db_constraint=False, verbose_name="章节") course = models.ForeignKey("Course", related_name="lesson_list", on_delete=models.DO_NOTHING, db_constraint=False, verbose_name="课程") class Meta: db_table = "fg_course_lesson" verbose_name = "课程课时" verbose_name_plural = verbose_name def __str__(self): return "%s-第%s章-%s-第%s课时-%s" % (self.course.name,self.chapter.orders, self.chapter.name, self.orders, self.name) def text2(self): return self.__str__() text2.short_description = "课时名称" text2.allow_tags = True text2.admin_order_field = "orders"View Code
提交版本
cd /home/moluo/Desktop/luffycity git add . git commit -m "feature: 安装配置simpleUI美化admin站点并使用富文本编辑器增强多行文本框" # git push git push --set-upstream origin feature/course
作业:在admin站点展示用户模型相关数据。
Admin站点配置
simpleui 公共配置,settings/dev.py,代码:
# admin站点公共配置 from django.contrib import admin admin.AdminSite.site_header = '浮光在线' admin.AdminSite.site_title = '浮光在线教育站点管理' # 登录界面logo地址 SIMPLEUI_LOGO = '/uploads/logo.png' # 快速操作 SIMPLEUI_HOME_QUICK = True # 服务器信息 SIMPLEUI_HOME_INFO = True # 关闭simpleui内置的使用分析 SIMPLEUI_ANALYSIS = False # 离线模式 SIMPLEUI_STATIC_OFFLINE = True # 首页图标地址 SIMPLEUI_INDEX = 'http://www.luffycity.cn:3000/'View Code
Admin站点关联外键数据
一个学习方向下面有多个课程分类,如果我们希望在查看或编辑某个学习方向的信息时,希望Admin站点在显示学习方向的信息的同时也一同显示并编辑同属该方向下的所有课程分类信息。我们可以使用django.admin提供的 TabularInline 和 StackedInline 内嵌类来实现。这2个类的使用一样,不同的是排版效果:
TabularInline让外键对应的数据横向排列(表格的一行),
StackedInline让外键对应的数据竖着排(表单格式)。
courses.admin,代码:
class CourseCategoryInLine(admin.StackedInline): """课程分类的内嵌类""" model = CourseCategory fields = ["id","name","orders"] class CourseDirectionModelAdmin(admin.ModelAdmin): """学习方向的模型管理器""" list_display = ["id","name","recomment_home_hot","recomment_home_top"] ordering = ["id"] list_filter = ["recomment_home_hot","recomment_home_top"] search_fields = ["name"] # 内嵌外键数据 inlines = [CourseCategoryInLine, ]View Code
给图片字段生成缩略图
在项目开发中,经常会遇到需要以图片的方式来展示商品/课程/物品/人物,但是如果每次展示的图片都是高清图片,则客户端访问页面时,下载这个图片就占据我们服务端的一定的网络资源,因为高清图片往往比较大,也可以影响用户访问页面的速度。让项目在列表中展示缩略图即可,真正的高清图直接在详情页中展示(例如京东商城)。
pip install django-stdimage
添加子应用,settings.dev,代码:
INSTALLED_APPS = [ 'simpleui', # admin界面美化,必须写在admin上面 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', "rest_framework", # 注意:记得加入 rest_framework 'corsheaders', # cors跨域子应用 'ckeditor', # 富文本编辑器 'ckeditor_uploader', # 富文本编辑器上传文件子用用 'stdimage', # 生成缩略图 "home", 'users', 'courses', ]View Code
courses.models,代码(将含有ImageField字段的进行设置缩略图):
from models import models, BaseModel from ckeditor.fields import RichTextField from ckeditor_uploader.fields import RichTextUploadingField from stdimage import StdImageField from django.utils.safestring import mark_safe # Create your models here. class CourseDirection(BaseModel): name = models.CharField(max_length=255, unique=True, verbose_name="方向名称") remark = RichTextUploadingField(default="", blank=True, null=True, verbose_name="方向描述") recomment_home_hot = models.BooleanField(default=False, verbose_name="是否推荐到首页新课栏目") recomment_home_top = models.BooleanField(default=False, verbose_name="是否推荐到首页必学栏目") class Meta: db_table = "fg_course_direction" verbose_name = "学习方向" verbose_name_plural = verbose_name def __str__(self): return self.name class CourseCategory(BaseModel): name = models.CharField(max_length=255, unique=True, verbose_name="分类名称") remark = RichTextUploadingField(default="", blank=True, null=True, verbose_name="分类描述") # related_name 反向引用属性名称 # 数据库外键设置为虚拟外键:db_constraint=False direction = models.ForeignKey("CourseDirection", related_name="category_list", on_delete=models.DO_NOTHING, db_constraint=False, verbose_name="学习方向") class Meta: db_table = "fg_course_category" verbose_name = "课程分类" verbose_name_plural = verbose_name def __str__(self): return self.name class Course(BaseModel): course_type = ( (0, '付费购买'), (1, '会员专享'), (2, '学位课程'), ) level_choices = ( (0, '初级'), (1, '中级'), (2, '高级'), ) status_choices = ( (0, '上线'), (1, '下线'), (2, '预上线'), ) # course_cover = models.ImageField(upload_to="course/cover", max_length=255, verbose_name="封面图片", blank=True, null=True) course_cover = StdImageField(variations={ 'thumb_1080x608': (1080, 608), # 高清图 'thumb_540x304': (540, 304), # 中等比例, 'thumb_108x61': (108, 61, True), # 小图(第三个参数表示保持图片质量), }, max_length=255, delete_orphans=True, upload_to="course/cover", null=True, verbose_name="封面图片",blank=True) course_video = models.FileField(upload_to="course/video", max_length=255, verbose_name="封面视频", blank=True, null=True) course_type = models.SmallIntegerField(choices=course_type,default=0, verbose_name="付费类型") level = models.SmallIntegerField(choices=level_choices, default=1, verbose_name="难度等级") description = RichTextUploadingField(null=True, blank=True, verbose_name="详情介绍") pub_date = models.DateField(auto_now_add=True, verbose_name="发布日期") period = models.IntegerField(default=7, verbose_name="建议学习周期(day)") attachment_path = models.FileField(max_length=1000, blank=True, null=True, verbose_name="课件路径") attachment_link = models.CharField(max_length=1000, blank=True, null=True, verbose_name="课件链接") status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="课程状态") students = models.IntegerField(default=0, verbose_name="学习人数") lessons = models.IntegerField(default=0, verbose_name="总课时数量") pub_lessons = models.IntegerField(default=0, verbose_name="已更新课时数量") price = models.DecimalField(max_digits=10,decimal_places=2, verbose_name="课程原价",default=0) recomment_home_hot = models.BooleanField(default=False, verbose_name="是否推荐到首页新课栏目") recomment_home_top = models.BooleanField(default=False, verbose_name="是否推荐到首页必学栏目") direction = models.ForeignKey("CourseDirection", related_name="course_list", on_delete=models.DO_NOTHING, null=True, blank=True, db_constraint=False, verbose_name="学习方向") category = models.ForeignKey("CourseCategory", related_name="course_list", on_delete=models.DO_NOTHING, null=True, blank=True, db_constraint=False, verbose_name="课程分类") teacher = models.ForeignKey("Teacher", related_name="course_list", on_delete=models.DO_NOTHING, null=True, blank=True, db_constraint=False, verbose_name="授课老师") class Meta: db_table = "fg_course_info" verbose_name = "课程信息" verbose_name_plural = verbose_name def __str__(self): return "%s" % self.name # 针对缩略图增加三个字段course_cover_small course_cover_medium course_cover_large def course_cover_small(self): if self.course_cover: return mark_safe(f'<img style="border-radius: 0%;" src="{self.course_cover.thumb_108x61.url}">') return "" course_cover_small.short_description = "封面图片(108x61)" course_cover_small.allow_tags = True course_cover_small.admin_order_field = "course_cover" def course_cover_medium(self): if self.course_cover: return mark_safe(f'<img style="border-radius: 0%;" src="{self.course_cover.thumb_540x304.url}">') return "" course_cover_medium.short_description = "封面图片(540x304)" course_cover_medium.allow_tags = True course_cover_medium.admin_order_field = "course_cover" def course_cover_large(self): if self.course_cover: return mark_safe(f'<img style="border-radius: 0%;" src="{self.course_cover.thumb_1080x608.url}">') return "" course_cover_large.short_description = "封面图片(1080x608)" course_cover_large.allow_tags = True course_cover_large.admin_order_field = "course_cover" class Teacher(BaseModel): role_choices = ( (0, '讲师'), (1, '导师'), (2, '班主任'), ) role = models.SmallIntegerField(choices=role_choices, default=0, verbose_name="讲师身份") title = models.CharField(max_length=64, verbose_name="职位、职称") signature = models.CharField(max_length=255, blank=True, null=True, verbose_name="导师签名") # avatar = models.ImageField(upload_to="teacher", null=True, verbose_name="讲师头像") # 使用缩略图提供的StdImageFiled字段以后,每次客户端提交图片时,stdImage模块会自动根据字段里面的配置项生成对应尺寸的缩略图 avatar = StdImageField(variations={ 'thumb_800x800': (800, 800), # 'large': (800, 800), 'thumb_400x400': (400, 400), # 'medium': (400, 400), 'thumb_50x50': (50, 50, True), # 'small': (50, 50, True), }, delete_orphans=True, upload_to="teacher", null=True, verbose_name="讲师头像") brief = RichTextUploadingField(max_length=1024, verbose_name="讲师描述") class Meta: db_table = "fg_teacher" verbose_name = "讲师信息" verbose_name_plural = verbose_name def __str__(self): return "%s" % self.name # 针对缩略图增加三个字段 def avatar_small(self): if self.avatar: return mark_safe(f'<img style="border-radius: 100%;" src="{self.avatar.thumb_50x50.url}">') return "" avatar_small.short_description = "头像信息(50x50)" avatar_small.allow_tags = True avatar_small.admin_order_field = "avatar" def avatar_medium(self): if self.avatar: return mark_safe(f'<img style="border-radius: 100%;" src="{self.avatar.thumb_400x400.url}">') return "" avatar_medium.short_description = "头像信息(400x400)" avatar_medium.allow_tags = True avatar_medium.admin_order_field = "avatar" def avatar_large(self): if self.avatar: return mark_safe(f'<img style="border-radius: 100%;" src="{self.avatar.thumb_800x800.url}">') return "" avatar_large.short_description = "头像信息(800x800)" avatar_large.allow_tags = True avatar_large.admin_order_field = "avatar" class CourseChapter(BaseModel): """课程章节""" orders = models.SmallIntegerField(default=1, verbose_name="第几章") summary = RichTextUploadingField(blank=True, null=True, verbose_name="章节介绍") pub_date = models.DateField(auto_now_add=True, verbose_name="发布日期") course = models.ForeignKey("Course", related_name='chapter_list', on_delete=models.CASCADE, db_constraint=False, verbose_name="课程名称") class Meta: db_table = "fg_course_chapter" verbose_name = "课程章节" verbose_name_plural = verbose_name def __str__(self): return "%s-第%s章-%s" % (self.course.name, self.orders, self.name) # 自定义字段 def text(self): return self.__str__() # admin站点配置排序规则和显示的字段文本提示 text.short_description = "章节名称" text.allow_tags = True text.admin_order_field = "orders" class CourseLesson(BaseModel): """课程课时""" lesson_type_choices = ( (0, '文档'), (1, '练习'), (2, '视频'), ) orders = models.SmallIntegerField(default=1, verbose_name="第几节") lesson_type = models.SmallIntegerField(default=2, choices=lesson_type_choices, verbose_name="课时种类") lesson_link = models.CharField(max_length=255, blank=True, null=True, help_text="若是video,填视频地址或者视频id,若是文档,填文档地址", verbose_name="课时链接") duration = models.CharField(blank=True, null=True, max_length=32, verbose_name="课时时长") # 仅在前端展示使用 pub_date = models.DateTimeField(auto_now_add=True, verbose_name="发布时间") free_trail = models.BooleanField(default=False, verbose_name="是否可试看") recomment = models.BooleanField(default=False, verbose_name="是否推荐到课程列表") chapter = models.ForeignKey("CourseChapter", related_name='lesson_list', on_delete=models.CASCADE, db_constraint=False, verbose_name="章节") course = models.ForeignKey("Course", related_name="lesson_list", on_delete=models.DO_NOTHING, db_constraint=False, verbose_name="课程") class Meta: db_table = "fg_course_lesson" verbose_name = "课程课时" verbose_name_plural = verbose_name def __str__(self): return "%s-%s" % (self.chapter, self.name) def text2(self): return self.__str__() text2.short_description = "课时名称" text2.allow_tags = True text2.admin_order_field = "orders"View Code
courses.admin,代码:
from django.contrib import admin from .models import CourseDirection, CourseCategory, Course, Teacher, CourseChapter, CourseLesson class CourseCategoryInLine(admin.StackedInline): """课程分类的内嵌类""" model = CourseCategory fields = ["id","name","orders"] class CourseDirectionModelAdmin(admin.ModelAdmin): """学习方向的模型管理器""" list_display = ["id","name","recomment_home_hot","recomment_home_top"] # 默认排序字段 ordering = ["id"] # 字段过滤 list_filter = ["recomment_home_hot", "recomment_home_top"] # 搜索字段 search_fields = ["name"] # 内嵌外键数据 inlines = [CourseCategoryInLine, ] # 分页配置,一夜数据量 list_per_page = 10 admin.site.register(CourseDirection, CourseDirectionModelAdmin) class CourseCategoryModelAdmin(admin.ModelAdmin): """课程分类的模型管理器""" list_display = ["id","name","direction"] ordering = ["id"] list_filter = ["direction"] search_fields = ["name"] # 分页配置,一页数据量 list_per_page = 10 # 更新数据时的表单配置项 fieldsets = ( ("必填", {'fields': ('name','direction', 'remark')}), ("选填", { 'classes': ('collapse',), 'fields': ('is_show', 'orders'), }), ) # 添加数据时的表单配置项 add_fieldsets = ( (None, { 'classes': ('wide',), 'fields': ('name', 'direction', 'remark'), }), ) # 当前方法会在显示表单的时候,自动执行,返回值就是表单配置项 def get_fieldsets(self, request, obj=None): """ 获取表单配置项 :param request: 客户端的http请求对象 :param obj: 本次修改的模型对象,如果是添加数据操作,则obj为None :return: """ if not obj: return self.add_fieldsets return super().get_fieldsets(request, obj) admin.site.register(CourseCategory, CourseCategoryModelAdmin) # 修改成缩略图的自定义字段 course_cover_small class CourseModelAdmin(admin.ModelAdmin): """课程信息的模型管理器""" list_display = ["id","name",'course_cover_small',"course_type","level","pub_date","students","lessons","price"] # 分页配置,一夜数据量 list_per_page = 10 admin.site.register(Course, CourseModelAdmin) # avatar_small class TeacherModelAdmin(admin.ModelAdmin): """讲师信息的模型管理器""" list_display = ["id","name","avatar_small","title","role","signature"] # 分页配置,一夜数据量 list_per_page = 10 # 搜索字段 search_fields = ["name", "title", "role", "signature"] admin.site.register(Teacher, TeacherModelAdmin) class CourseChapterModelAdmin(admin.ModelAdmin): """课程章节的模型管理器""" list_display = ["id","text", "pub_date",] # 分页配置,一夜数据量 list_per_page = 10 admin.site.register(CourseChapter, CourseChapterModelAdmin) class CourseLessonModelAdmin(admin.ModelAdmin): """课程课时的模型管理器""" list_display = ["id","text", "text2", "lesson_type", "duration", "pub_date", "free_trail"] # 分页配置,一夜数据量 list_per_page = 10 # 下面是旧版本写法,django2.0版本 -> django3.0以后,建议在模型中声明自定义字段 ---> text2属于新版本写法 def text(self, obj): return obj.__str__() text.admin_order_field = "orders" text.short_description = "课时名称" admin.site.register(CourseLesson, CourseLessonModelAdmin) 完整的course.admin,代码: from django.contrib import admin from .models import CourseDirection, CourseCategory, Course, Teacher, CourseChapter, CourseLesson # Register your models here. class CourseCategoryInLine(admin.StackedInline): """课程分类的内嵌类""" model = CourseCategory fields = ["id","name","orders"] class CourseDirectionModelAdmin(admin.ModelAdmin): """学习方向的模型管理器""" list_display = ["id","name","recomment_home_hot","recomment_home_top"] # 默认排序字段 ordering = ["id"] # 字段过滤 list_filter = ["recomment_home_hot", "recomment_home_top"] # 搜索字段 search_fields = ["name"] # 内嵌外键数据 inlines = [CourseCategoryInLine, ] # 分页配置,一夜数据量 list_per_page = 10 admin.site.register(CourseDirection, CourseDirectionModelAdmin) class CourseCategoryModelAdmin(admin.ModelAdmin): """课程分类的模型管理器""" list_display = ["id","name","direction"] ordering = ["id"] list_filter = ["direction"] search_fields = ["name"] # 分页配置,一夜数据量 list_per_page = 10 # 更新数据时的表单配置项 fieldsets = ( ("必填", {'fields': ('name','direction', 'remark')}), ("选填", { 'classes': ('collapse',), 'fields': ('is_show', 'orders'), }), ) # 添加数据时的表单配置项 add_fieldsets = ( (None, { 'classes': ('wide',), 'fields': ('name', 'direction', 'remark'), }), ) # 当前方法会在显示表单的时候,自动执行,返回值就是表单配置项 def get_fieldsets(self, request, obj=None): """ 获取表单配置项 :param request: 客户端的http请求对象 :param obj: 本次修改的模型对象,如果是添加数据操作,则obj为None :return: """ if not obj: return self.add_fieldsets return super().get_fieldsets(request, obj) admin.site.register(CourseCategory, CourseCategoryModelAdmin) class CourseModelAdmin(admin.ModelAdmin): """课程信息的模型管理器""" list_display = ["id","name",'course_cover_small',"course_type","level","pub_date","students","lessons","price"] # 分页配置,一夜数据量 list_per_page = 10 admin.site.register(Course, CourseModelAdmin) class TeacherModelAdmin(admin.ModelAdmin): """讲师信息的模型管理器""" list_display = ["id","name","avatar_small","title","role","signature"] # 分页配置,一夜数据量 list_per_page = 10 # 搜索字段 search_fields = ["name", "title", "role", "signature"] admin.site.register(Teacher, TeacherModelAdmin) class CourseChapterModelAdmin(admin.ModelAdmin): """课程章节的模型管理器""" list_display = ["id","text", "pub_date",] # 分页配置,一夜数据量 list_per_page = 10 admin.site.register(CourseChapter, CourseChapterModelAdmin) class CourseLessonModelAdmin(admin.ModelAdmin): """课程课时的模型管理器""" list_display = ["id","text", "text2", "lesson_type", "duration", "pub_date", "free_trail"] # 分页配置,一夜数据量 list_per_page = 10 # 下面是旧版本写法,django2.0版本 -> django3.0以后,建议在模型中声明自定义字段 ---> text2属于新版本写法 def text(self, obj): return obj.__str__() text.admin_order_field = "orders" text.short_description = "课时名称" admin.site.register(CourseLesson, CourseLessonModelAdmin)View Code
提交版本
cd /home/moluo/Desktop/luffycity git add . git commit -m "feature: admin站点配置以及给图片字段生成缩略图" git push
测试数据
学习方向:
truncate table fg_course_direction; INSERT INTO luffycity.fg_course_direction (id,orders, is_show, is_deleted, created_time, updated_time, name, remark, recomment_home_hot, recomment_home_top) VALUES (1,1, 1, 0, '2021-07-22 05:42:01.290060', '2021-07-22 05:42:01.290088', '前端开发', '', 1, 1), (2,1, 1, 0, '2021-07-22 05:42:01.290060', '2021-07-22 05:42:01.290088', '后端开发', '', 1, 1), (3,1, 1, 0, '2021-07-22 05:42:01.290060', '2021-07-22 05:42:01.290088', '移动开发', '', 1, 1), (4,1, 1, 0, '2021-07-22 05:42:01.290060', '2021-07-22 05:42:01.290088', '计算机基础', '', 1, 1), (5,1, 1, 0, '2021-07-22 05:42:01.290060', '2021-07-22 05:42:01.290088', '前沿技术', '', 1, 1), (6,1, 1, 0, '2021-07-22 05:42:01.290060', '2021-07-22 05:42:01.290088', '云计算', '', 1, 1), (7,1, 1, 0, '2021-07-22 05:42:01.290060', '2021-07-22 05:42:01.290088', '大数据', '', 1, 1), (8,1, 1, 0, '2021-07-22 05:42:01.290060', '2021-07-22 05:42:01.290088', '运维', '', 1, 1), (9,1, 1, 0, '2021-07-22 05:42:01.290060', '2021-07-22 05:42:01.290088', '测试', '', 1, 1), (10,1, 1, 0, '2021-07-22 05:42:01.290060', '2021-07-22 05:42:01.290088', '数据库', '', 1, 1), (11,1, 1, 0, '2021-07-22 05:42:01.290060', '2021-07-22 05:42:01.290088', 'UI设计', '', 1, 1), (12,1, 1, 0, '2021-07-22 05:42:01.290060', '2021-07-22 05:42:01.290088', '多媒体', '', 1, 1), (13,1, 1, 0, '2021-07-22 05:42:01.290060', '2021-07-22 05:42:01.290088', '游戏', '', 1, 1), (14,1, 1, 0, '2021-07-22 05:42:01.290060', '2021-07-22 05:42:01.290088', '求职面试', '', 1, 1);View Code
课程分类:
truncate table fg_course_category; INSERT INTO luffycity.fg_course_category (orders, is_show, is_deleted, created_time, updated_time, name, remark, direction_id) VALUES (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'Vue.js', '', 1), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'Typescript', '', 1), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'React.js', '', 1), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'HTML', '', 1), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'JavaScript', '', 1), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'Angular', '', 1), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'Node.js', '', 1), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'WebApp', '', 1), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', '小程序', '', 1), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', '前端工具', '', 1), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'HTML/CSS', '', 1), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'Html5', '', 1), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'CSS3', '', 1), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'Java', '', 2), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'SpringBoot', '', 2), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'Spring Cloud', '', 2), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'SSM', '', 2), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'PHP', '', 2), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', '.net', '', 2), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'Python', '', 2), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', '爬虫', '', 2), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'Django', '', 2), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'Flask', '', 2), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'Go', '', 2), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'C', '', 2), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'C++', '', 2), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'C#', '', 2), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'Flutter', '', 3), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'Android', '', 3), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'iOS', '', 3), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'React native', '', 3), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', '计算机网络', '', 4), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', '算法与数据结构', '', 4), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', '数学', '', 4), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', '微服务', '', 5), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', '机器学习', '', 5), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', '深度学习', '', 5), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', '计算机视觉', '', 5), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', '自然语言处理', '', 5), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', '数据分析&挖掘', '', 5), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', '大数据', '', 6), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'Hadoop', '', 6), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'Spark', '', 6), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'Hbase', '', 6), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'Flink', '', 6), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'Storm', '', 6), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', '阿里云', '', 7), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', '容器', '', 7), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'Docker', '', 7), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'Kubernetes', '', 7), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', '运维', '', 8), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', '自动化运维', '', 8), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', '中间件', '', 8), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'Linux', '', 8), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', '测试', '', 9), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', '功能测试', '', 9), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', '性能测试', '', 9), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', '自动化测试', '', 9), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', '接口测试', '', 9), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'MySQL', '', 10), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'Redis', '', 10), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'MongoDB', '', 10), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', '设计基础', '', 11), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', '设计工具', '', 11), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'APPUI设计', '', 11), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'Unity 3D', '', 13), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'cocos creator', '', 13), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', '求职面试', '', 14), (1, 1, 0, '2021-07-22 08:00:19.366304', '2021-07-22 08:00:19.367343', 'leetcode', '', 14);View Code
讲师信息:
truncate table fg_teacher; INSERT INTO luffycity.fg_teacher (id, name, orders, is_show, is_deleted, created_time, updated_time, role, title, signature, avatar, brief) VALUES (1, '张老师', 1, 1, 0, '2021-07-22 04:31:27.741562', '2021-07-22 04:31:27.741708', 0, 'BAT中某某技术总监', 'xxxxxxxx', 'teacher/avatar.jpg', '<p>2009入行,在IT行业深耕13年,删库无数,行内同行送称号:删库小王子。</p>'), (2, '李老师', 1, 1, 0, '2021-07-22 04:31:27.741562', '2021-07-22 04:31:27.741708', 0, 'BAT中某某技术顾问', 'xxxxxxxx', 'teacher/avatar.jpg', '<p>百变小王子,各种框架信手拈来。</p>'), (3, '王老师', 1, 1, 0, '2021-07-22 04:31:27.741562', '2021-07-22 04:31:27.741708', 0, 'BAT中某某技术主管', 'xxxxxxxx', 'teacher/avatar.jpg', '<p>草根站长,专注运维20年。</p>'), (4, '红老师', 1, 1, 0, '2021-07-22 04:31:27.741562', '2021-07-22 04:31:27.741708', 0, 'BAT中某某项目经理', 'xxxxxxxx', 'teacher/avatar.jpg', '<p>美女讲师,说话好听。</p>');View Code
添加测试数据在工作中一共有三种方式:
-
可以根据django的manage.py指令进行[自定义终端命令]
-
可以采用第三方模块Faker来完成数据的模拟添加(Faker模块可以写在上面第一种方法里面)
-
可以使用python脚本或者shell脚本来完成
第一种自定义终端命令
文档:https://docs.djangoproject.com/zh-hans/3.2/howto/custom-management-commands/
在子应用目录下添加一个 management/commands
包目录,然后这个commands里面处理__init__.py
以外其他文件的文件名将作为django-admin或者python manage.py的命令选项了。注意:文件名不能以_开头,否则不会被识别为命令。
接着我们可以在commands包下根据自己的业务需要,编写对应的命令。例如,我们现在需要在courses子应用下添加课程对应的测试数据,
所以我们在courses下创建 management/commands
包目录(包含init文件的),然后创建命令文件example.py,代码:
import constants from django.core.management.base import BaseCommand, CommandError from courses.models import Teacher # 类名必须是Command而且一个文件就是一个命令类,这个命令类必须直接或间接继承BaseCommand class Command(BaseCommand): # 就是命令的帮助信息 help = '添加课程相关的测试数据' # 如果当前命令,需要接受来自终端的参数,可以使用add_arguments def add_arguments(self, parser): pass # 位置参数,必填项 # parser.add_argument('name', nargs='+', type=int) # 命令参数,可选项 # parser.add_argument( # '--table', # action='store_true', # help='Delete poll instead of closing it', # ) # 命令执行的核心方法,实行命令会自动调用此方法 def handle(self, *args, **options): """添加测试数据""" print("添加测试数据") # 需要添加的测试数据 多个用for循环 Teacher.objects.create( name="赵小明", avatar="teacher/avatar.jpg", role=1, title="老师", signature="从业3年,管理班级无数", brief="从业3年,管理班级无数", )View Code
终端输入命令(执行hander方法添加测试数据):
python manage.py example
第二种Faker添加模拟数据
文档:https://faker.readthedocs.io/en/stable/locales/zh_CN.html#
github:https://github.com/joke2k/faker/
faker是一个在多个编程语言里面都比较常用的第三方工具类,它的作用就是可以提供非常有效的方式帮开发者生成一些模拟仿真的测试数据。(通过faker的自有方法可以生成逼真的数据信息)
pip install faker
结合上面的自定义终端命令来实现,example.py,代码:
import constants,random from django.core.management.base import BaseCommand, CommandError from courses.models import Teacher from faker import Faker from django.conf import settings # 类名必须是Command而且一个文件就是一个命令类,这个命令类必须直接或间接继承BaseCommand class Command(BaseCommand): help = '添加课程相关的测试数据' # 如果当前命令,需要接受来自终端的参数,可以使用add_arguments def add_arguments(self, parser): # 位置参数,必填项 # parser.add_argument('date_type', nargs='+', type=int, help="添加数据的类型") # 命令参数,可选项 # 不指定类型默认就是teacher类型 parser.add_argument( '--type', dest='type', default='teacher', type=str, help='测试数据的类型', ) # 不指定number就是生成10个数据 parser.add_argument( '--number', dest='number', default=10, type=int, help='添加数据的数量', ) # 命令执行的核心方法, def handle(self, *args, **options): """添加课程相关的测试数据""" if options["type"] == "teacher": # 调用add_teacher self.add_teacher(options) elif options["type"] == "direction": self.add_direction(options) def add_teacher(self,options): """添加授课老师的测试数据""" # 实例化faker模块 faker = Faker(["zh_CN"]) for i in range(options["number"]): Teacher.objects.create( name=faker.unique.name(), # 唯一的姓名 avatar="teacher/avatar.jpg", role=random.randint(0,2), title="老师", signature= "从业3年,管理班级无数", brief= f"从业3年,管理班级无数,联系电话:{faker.unique.phone_number()},邮箱地址:{faker.unique.company_email()}", ) print("添加授课老师的测试数据完成....") def add_direction(self,options): """添加学习方向的测试数据""" print("添加学习方向的测试数据完成....")View Code
终端下调用:
cd ~/Desktop/luffycity/luffycityapi # 默认生成10个老师 python manage.py example # 指定生成10个老师 python manage.py example --type=teacher # 指定学习方向类型生成10个学习方向 python manage.py example --type=direction、 # 指定生成100个老师 python manage.py example --type=teacher --number=100
第三种基于终端脚本来完成数据的添加
要编写一个python或者shell脚本,就要清楚一件事情,就是我们可以根据对应的语言来编写对应的终端代码,但是必须在首行的位置声明执行这些代码的解析器是谁?路径在哪里?
编写python脚本(最重要的是首行代码)
首行指定运行当前代码的python解释器。写完整绝对路径。(作用就是找到python解释器运行此文件)
例如: #! /home/moluo/anaconda3/envs/luffycity/bin/python
终端输入:(查单python解释器的路径)
whereis python
结果:
python: /usr/bin/python3.8 /usr/lib/python3.9 /usr/lib/python3.8 /usr/lib/python2.7 /etc/python3.8 /usr/local/lib/python3.8 /usr/include/python3.8 /home/moluo/anaconda3/envs/luffycity/bin/python3.9 /home/moluo/anaconda3/envs/luffycity/bin/python /home/moluo/anaconda3/envs/luffycity/bin/python3.9-config # 这些路径都可以用放在脚本的第一行
scripts/test1.py,代码:
#! /home/moluo/anaconda3/envs/luffycity/bin/python # 首行以后的代码必须要符合python的语法 """ 针对通用代码的运行,可以直接使用系统内置的全局环境的python解释器,也可以使用虚拟环境的解析器 #! /usr/bin/python3 如果这个代码需要调用对应的第三方模块,那么就要写上安装该模块的python解释器 #! /home/moluo/anaconda3/envs/luffycity/bin/python """ # 执行下面的功能脚本 import os, sys from faker import Faker faker = Faker(["zh_CN"]) user = faker.unique.name() print(f"hello,{user}") # python获取终端参数 # 终端输入:./test1.py user try: dir = sys.argv[1] # 0 ==> ./test1.py 1 ==> user # print(dir) # user except Exception as e: dir = "2021" # python可在终端直接执行shell命令 # 等同于终端直接输入命令ls -l ret = os.popen("ls -l") # 等同于上一行 print(ret.read()) # 执行命令:mkdir {dir}创建文件夹,cd {dir}进入文件夹,echo 'hello {user}' > index.html将hello {user}写入文件中 ret = os.popen(f"mkdir {dir} && cd {dir} && echo 'hello {user}' > index.html") # 等同于上一行 print(ret.read())View Code
编写shell脚本(终端上写的命令关闭后就没有了,脚本可以一直用)
首行指定运行当前代码的shell解释器。写完整路径。
scripts/test2.sh,代码:(只要平时在终端用的命令都可以直接写到文件中)
#! /bin/bash ls -l .... # 等同于在在终端执行命令
在终端执行文件:
./test2.sh
执行脚本需要权限
不管编写的什么的脚本命令,编写完脚本以后,脚本本身因为操作系统默认会取消它的执行权限,所以我们通过以下命令来增加执行的权限。
chmod +x 对应的文件名 # chmod 755 对应的文件名 # 例如,给python脚本赋予执行的权限。文件名假设为:test1.py chmod +x test1.py # 例如,shell脚本赋予执行的权限,文件名假设为:test2.sh chmod +x test2.sh
赋予了权限以后,就可以执行脚本了。
但是执行过程中, 一定使用相对路径的方式来执行这个脚本。
# 例如,上面的test1.py或者test2.sh ./test1.py ./test2.sh # 如果不希望使用相对路径,则需要把当前文件所载的目录设置为环境变量才行。
使用shell命令来完成测试数据的添加
新建文件 new file(不是创建py文件)
-
编写一个sql语句的文件test_data.sql:
-- 如果使用数据库本身的外键,则添加/删除/修改数据时,务必关闭原来表中的主外键约束功能 set FOREIGN_KEY_CHECKS=0; -- 清空原有的课程信息表信息 truncate table fg_course_info; -- 添加课程信息 INSERT INTO luffycity.fg_course_info (id, name, orders, is_show, is_deleted, created_time, updated_time, course_cover, course_video, course_type, level, description, pub_date, period, attachment_path, attachment_link, status, students, lessons, pub_lessons, price, recomment_home_hot, recomment_home_top, category_id, direction_id, teacher_id) VALUES (1, '7天Typescript从入门到放弃', 1, 1, 0, '2021-07-22 04:35:05.696823', '2021-07-22 04:35:05.696871', 'course/cover/course-10.png', '', 0, 0, '<p>7天Typescript从入门到放弃</p>', '2021-07-22', 7, 'luffycity-celery用法1.zip', null, 0, 988, 100, 30, 998.00, 1, 1, 2, 1, 1), (2, '3天Typescript精修', 1, 1, 0, '2021-07-22 04:35:05.696823', '2021-07-22 04:35:05.696871', 'course/cover/course-9.png', '', 0, 0, '<p>3天Typescript精修</p>', '2021-07-22', 7, 'luffycity-celery用法1.zip', null, 0, 988, 100, 30, 998.00, 1, 1, 2, 1, 1), (3, '3天学会Vue基础', 1, 1, 0, '2021-07-22 04:35:05.696823', '2021-07-22 04:35:05.696871', 'course/cover/course-8.png', '', 0, 0, '<p>3天学会Vue基础</p>', '2021-07-22', 7, 'luffycity-celery用法1.zip', null, 0, 988, 100, 30, 998.00, 1, 1, 2, 1, 1); -- 如果使用数据库本身的外键,则添加/删除/修改数据以后,务必开启原来表中的主外键约束功能 set FOREIGN_KEY_CHECKS=1;
View Code -
编写一个shell脚本test_data.sh来执行上面的文件:
mysql -uroot -p123 luffycity < ./test_data.sql 。数据库用户名密码库名 < 要执行的执行的文件命令
#! /bin/bash mysql -uroot -p123 luffycity < ./test_data.sql
-
赋予create_data.sh执行的权限:
chmod +x test_data.sh ./test_data.sh
提交代码版本
cd /home/moluo/Desktop/luffycity/ git add . git commit -m "test: 添加测试数据的三种方式" git push
在Admin站点中管理公共数据与用户数据
home.apps,代码:
from django.apps import AppConfig class HomeConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'home' verbose_name="公共数据" verbose_name_plural = verbose_nameView Code
home.admin,代码:
from django.contrib import admin from .models import Nav, Banner # Register your models here. class NavModelAdmin(admin.ModelAdmin): """导航菜单的模型管理器""" list_display = ["id","name","link","is_http"] admin.site.register(Nav, NavModelAdmin) class BannerModelAdmin(admin.ModelAdmin): """轮播广告的模型管理器""" list_display = ["id","image_html","link","is_http"] admin.site.register(Banner, BannerModelAdmin)View Code
home.models,代码:
from models import BaseModel, models from django.utils.safestring import mark_safe # Create your models here. class Nav(BaseModel): """导航菜单""" # 字段选项 # 模型对象.<字段名> ---> 实际数据 # 模型对象.get_<字段名>_display() --> 文本提示 POSITION_CHOICES = ( # (实际数据, "文本提示"), (0, "顶部导航"), (1, "脚部导航"), ) link = models.CharField(max_length=255, verbose_name="导航连接") is_http = models.BooleanField(default=False, verbose_name="是否站外连接地址") position = models.SmallIntegerField(default=0, choices=POSITION_CHOICES, verbose_name="导航位置") class Meta: db_table = "fg_nav" verbose_name = "导航菜单" verbose_name_plural = verbose_name class Banner(BaseModel): # models.ImageField 表示该字段的内容,按图片格式进行处理,通过upload_to进行指定保存的目录 # 图片的最终路径 = settings.MEDIA_ROOT / upload_to / 文件名 # upload_to 支持格式化符号,%Y 表示年份 %m 表示月份,%d 表示日 image = models.ImageField(upload_to="banner/%Y/", verbose_name="图片地址") link = models.CharField(max_length=500, verbose_name="链接地址") note = models.CharField(max_length=150, verbose_name='备注信息') is_http = models.BooleanField(default=False, verbose_name="是否外链地址", help_text="站点链接地址:http://www.baidu.com/book<br>站点链接地址:/book/") class Meta: db_table = "fg_banner" verbose_name = "轮播广告" verbose_name_plural = verbose_name def image_html(self): if self.image: # 防止转义 return mark_safe(f'<img style="border-radius: 0%;max-height: 100px; max-width: 400px;" src="{self.image.url}">') return "" image_html.short_description = "广告图片" image_html.allow_tags = True image_html.admin_order_field = "image"View Code
users/apps,代码:
from django.apps import AppConfig class UsersConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'users' verbose_name="用户管理" verbose_name_plural = verbose_name View Code
users.admin,代码:
from django.contrib import admin from django.contrib.auth.admin import UserAdmin, _ from .models import User # Register your models here. class UserModelAdmin(UserAdmin): list_display = ["id", "username", "avatar_small", "money", "credit", "mobile"] # fieldsets 和 add_fieldsets 都在从UserAdmin中复制粘贴过来,重写加上自己需要的字段的。 fieldsets = ( (None, {'fields': ('username', 'password', 'avatar')}), (_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}), (_('Permissions'), { 'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions'), }), (_('Important dates'), {'fields': ('last_login', 'date_joined')}), ) add_fieldsets = ( (None, { 'classes': ('wide',), 'fields': ('username', 'password1', 'password2'), }), ) ordering = ('id',) admin.site.register(User, UserModelAdmin)View Code
users.models,代码:
# Create your models here. from django.contrib.auth.models import AbstractUser,models from stdimage import StdImageField from django.utils.safestring import mark_safe class User(AbstractUser): mobile = models.CharField(max_length=15, unique=True, verbose_name='手机号') money = models.DecimalField(max_digits=9, default=0.0, decimal_places=2, verbose_name="钱包余额") credit = models.IntegerField(default=0, verbose_name="积分") # avatar = models.ImageField(upload_to="avatar/%Y", null=True, default="", verbose_name="个人头像") avatar = StdImageField(variations={ 'thumb_400x400': (400, 400), # 'medium': (400, 400), 'thumb_50x50': (50, 50, True), # 'small': (50, 50, True), }, delete_orphans=True, upload_to="avatar/%Y", blank=True, null=True, verbose_name="个人头像") class Meta: db_table = 'fg_users' verbose_name = '用户信息' verbose_name_plural = verbose_name def avatar_small(self): if self.avatar: return mark_safe( f'<img style="border-radius: 100%;" src="{self.avatar.thumb_50x50.url}">' ) return "" avatar_small.short_description = "个人头像(50x50)" avatar_small.allow_tags = True avatar_small.admin_order_field = "avatar" def avatar_medium(self): if self.avatar: return mark_safe( f'<img style="border-radius: 100%;" src="{self.avatar.thumb_400x400.url}">' ) return "" avatar_medium.short_description = "个人头像(400x400)" avatar_medium.allow_tags = True avatar_medium.admin_order_field = "avatar"View Code
提交代码版本
cd /home/moluo/Desktop/luffycity/ git add . git commit -m "feature: 在Admin站点中管理公共数据与用户数据" git push
实现图片上传到阿里云OSS对象存储
降低服务器的压力
部分公司:自己基于fastDFS构建静态资源服务器
部分公司:申请第三方云存储:阿里云OSS,腾讯云云存储,百度云云存储,亚马逊S3
创建阿里云OSS对象存储
开发文档:https://promotion.aliyun.com/ntms/act/ossdoclist.html?spm=5176.8465980.entries.1.4e701450wyVJSM
Bucket存储库:https://oss.console.aliyun.com/bucket
bucket luffycityoline endpoint oss-cn-beijing.aliyuncs.com
查询获取接口访问key和秘钥
地址:https://ram.console.aliyun.com/manage/ak
ACCESS_KEY_ID LTAI5t991uBJjk8TunKooM7M ACCESS_KEY_SECRET oEDvV9RaoCf6rHIZXlJCJAmk0phub2
安装阿里云的SDK集成到项目中使用
终端下安装:
pip install oss2 pip install django-oss-storage
python直接操作oss2
适用于一些没有oss集成模块的web框架中(这是一个demo文件),ossdemo.py,代码:
# uuid生成名称 import oss2,uuid if __name__ == '__main__': # 四个配置信息 OSS_ACCESS_KEY_ID = "LTAI5t991uBJjk8TunKooM7M" OSS_ACCESS_KEY_SECRET = "oEDvV9RaoCf6rHIZXlJCJAmk0phub2" OSS_ENDPOINT = "oss-cn-beijing.aliyuncs.com" # 访问域名, 根据服务器上的实际配置修改 OSS_BUCKET_NAME = "luffycityoline" # oss 创建的 BUCKET 名称 # 拼接服务段地址 OSS_SERVER_URL = f"https://{OSS_BUCKET_NAME}.{OSS_ENDPOINT}" # 创建命名空间操作实例对象 auth = oss2.Auth(OSS_ACCESS_KEY_ID, OSS_ACCESS_KEY_SECRET) bucket = oss2.Bucket(auth, OSS_ENDPOINT, OSS_BUCKET_NAME) # 上传文件 # 指定文件路径OSS云存储的路径 image = f"demo/{str(uuid.uuid4())}.jpg" with open('/home/moluo/Desktop/luffycity/luffycityapi/luffycityapi/uploads/avatar/2021/avatar.jpg', "rb") as f: # 上传阿里云 result = bucket.put_object(image, f.read() ) print(result) print(result.status) print(f"{OSS_SERVER_URL}/{image}")View Code
django配置自定义文件存储上传文件到oss
settings.dev,代码:
# 阿里云OSS云存储 OSS_ACCESS_KEY_ID = "LTAI5t991uBJjk8TunKooM7M" OSS_ACCESS_KEY_SECRET = "oEDvV9RaoCf6rHIZXlJCJAmk0phub2" OSS_ENDPOINT = "oss-cn-beijing.aliyuncs.com" # 访问域名, 根据服务器上的实际配置修改 OSS_BUCKET_NAME = "luffycityoline" # oss 创建的 BUCKET 名称 # 添加下面配置后 Django admin 后台上传的 ImageField, FileField 类型的字段都会被自动上传到 oss 的服务器中, 访问路径也会自动替换 # 如果注释掉的话 oss 的配置会失效, 上传文件会存储到本地, 且访问路径也会变成本地 DEFAULT_FILE_STORAGE = 'django_oss_storage.backends.OssMediaStorage'View Code
注意:上面的配置完成以后,将来django中所有上传下载的文件都会默认从OSS对象存储中操作。所以本地原来保存的图片等静态资源再访问就无效了。所以我们需要把uploads这个目录下的所有文件信息,手动上传到当前项目配置的OSS Bucket存储库中。
将图片手动上传到云存储上:(项目就可以看图片了)
提交代码版本
cd /home/moluo/Desktop/luffycity/ git add . git commit -m "feature: 实现图片上传到阿里云OSS对象存储" git push
后端实现学习方向列表接口
序列化器
courses/serializers.py,代码:
from rest_framework import serializers from .models import CourseDirection class CourseDirectionModelSerializer(serializers.ModelSerializer): """学习方向的序列化器""" class Meta: model = CourseDirection fields = ["id", "name"]View Code
视图
courses.views,代码:
from rest_framework.generics import ListAPIView from .models import CourseDirection from .serializers import CourseDirectionModelSerializer # Create your views here. class CourseDirectionListAPIView(ListAPIView): """学习方向""" queryset = CourseDirection.objects.filter(is_show=True, is_deleted=False).order_by("orders","-id") serializer_class = CourseDirectionModelSerializerView Code
路由
courses.urls,代码:
from django.urls import path,re_path from . import views urlpatterns = [ path("directions/", views.CourseDirectionListAPIView.as_view()), ]View Code
提交代码版本
cd /home/moluo/Desktop/luffycity/ git add . git commit -m "feature: 服务端提供学习方向列表的api接口" git push
后端
实现课程分类列表接口
创建序列化器
courses/serializers.py
from rest_framework import serializers from .models import CourseDirection, CourseCategory class CourseDirectionModelSerializer(serializers.ModelSerializer): """学习方向的序列化器""" class Meta: model = CourseDirection fields = ["id", "name"] class CourseCategoryModelSerializer(serializers.ModelSerializer): """课程分类的序列化器""" class Meta: model = CourseCategory fields = ["id", "name"]View Code
视图
courses/views.py
from rest_framework.generics import ListAPIView from .models import CourseDirection, CourseCategory from .serializers import CourseDirectionModelSerializer, CourseCategoryModelSerializer # Create your views here. class CourseDirectionListAPIView(ListAPIView): """学习方向""" queryset = CourseDirection.objects.filter(is_show=True, is_deleted=False).order_by("orders", "-id") serializer_class = CourseDirectionModelSerializer pagination_class = None class CourseCategoryListAPIView(ListAPIView): """学习分类""" queryset = CourseCategory.objects.filter(is_show=True, is_deleted=False).order_by("orders","-id") serializer_class = CourseCategoryModelSerializer pagination_class = None View Code
路由
from django.urls import path,re_path from . import views urlpatterns = [ path("directions/", views.CourseDirectionListAPIView.as_view()), path("categories/", views.CourseCategoryListAPIView.as_view()), ]View Code
提交代码版本
cd /home/moluo/Desktop/luffycity/ git add . git commit -m "feature: 服务端提供课程分类列表的api接口" git push
客户端
发送请求获取学习方向和课程分类信息
src/api/course.js,代码:
import http from "../utils/http"; import {reactive, ref} from "vue" const course = reactive({ current_direction: 0, // 当前选中的学习方向,0表示所有方向 current_category: 0, // 当前选中的课程分类,0表示不限分类 direction_list: [], // 学习方向列表 category_list: [], // 课程分类列表 get_course_direction(){ // 获取学习方向信息 return http.get("/courses/directions/") }, get_course_category () { // 获取课程分类信息 return http.get('/courses/categories/') } }) export default course;View Code
src/views/Courses.vue,代码:
<template> <div class="course"> <Header></Header> <div class="top-wrap"> <div class="actual-header"> <div class="actual-header-wrap"> <div class="banner"> <router-link class="title" to="/course"><img class="h100" src="../assets/coding-title.png" alt=""></router-link> <div>真实项目实战演练</div> </div> <div class="actual-header-search"> <div class="search-inner"> <input class="actual-search-input" placeholder="搜索感兴趣的实战课程内容" type="text" autocomplete="off"> <img class="actual-search-button" src="../assets/search.svg" /> </div> <div class="actual-searchtags"> </div> <div class="search-hot"> <span>热搜:</span> <a href="">Java工程师</a> <a href="">Vue</a> </div> </div> </div> </div> <div class="type"> <div class="type-wrap"> <div class="one warp"> <span class="name">方向:</span> <ul class="items"> <li :class="{cur:course.current_direction===0}" @click.prevent.stop="course.current_direction=0"><a href="">全部</a></li> <li :class="{cur:course.current_direction===direction.id}" @click.prevent.stop="course.current_direction=direction.id" v-for="direction in course.direction_list"><a href="">{{direction.name}}</a></li> </ul> </div> <div class="two warp"> <span class="name">分类:</span> <ul class="items"> <li :class="{cur:course.current_category===0}"><a href="" @click.prevent.stop="course.current_category=0">不限</a></li> <li :class="{cur:course.current_category===category.id}" v-for="category in course.category_list"><a href="" @click.prevent.stop="course.current_category=category.id">{{category.name}}</a></li> </ul> </div> </div> </div> </div> <div class="main"> <div class="main-wrap"> <div class="filter clearfix"> <div class="sort l"> <a href="" class="on">最新</a> <a href="">销量</a> <a href="">升级</a> </div> <div class="other r clearfix"><a class="course-line l" href="" target="_blank">学习路线</a></div> </div> <ul class="course-list clearfix"> <li class="course-card"> <a target="_blank" href=""> <div class="img"><img src="../assets/course-1.png" alt=""></div> <p class="title ellipsis2">全面的Docker 系统性入门+进阶实践(2021最新版)</p> <p class="one"> <span>进阶 · 611人报名</span> <span class="discount r"><i class="name">优惠价</i></span> </p> <p class="two clearfix"> <span class="price l red bold">¥428.00</span> <span class="origin-price l delete-line">¥488.00</span> <span class="add-shop-cart r"><img class="icon imv2-shopping-cart" src="../assets/cart2.svg">加购物车</span> </p> </a> </li> <li class="course-card"> <a target="_blank" href=""> <div class="img"><img src="../assets/course-2.png" alt=""></div> <p class="title ellipsis2">Flink+ClickHouse 玩转企业级实时大数据开发,助你实现弯道超车</p> <p class="one"> <span>进阶 · 246人报名</span> <span class="discount r"><i class="name">限时优惠</i><i class="countdown">6<span class="day">天</span>01:39:21</i></span> </p> <p class="two clearfix"> <span class="price l red bold">¥328.00</span> <span class="origin-price l delete-line">¥368.00</span> <span class="add-shop-cart r"><img class="icon imv2-shopping-cart" src="../assets/cart2.svg">加购物车</span> </p> </a> </li> <li class="course-card"> <a target="_blank" href=""> <div class="img"><img src="../assets/course-3.png" alt=""></div> <p class="title ellipsis2">Flink+ClickHouse 玩转企业级实时大数据开发,助你实现弯道超车</p> <p class="one"> <span>进阶 · 246人报名</span> <span class="discount r"><i class="name">限时优惠</i><i class="countdown">16<span class="day">天</span>01:39:21</i></span> </p> <p class="two clearfix"> <span class="price l red bold">¥328.00</span> <span class="origin-price l delete-line">¥368.00</span> <span class="add-shop-cart r"><img class="icon imv2-shopping-cart" src="../assets/cart2.svg">加购物车</span> </p> </a> </li> <li class="course-card"> <a target="_blank" href=""> <div class="img"><img src="../assets/course-4.png" alt=""></div> <p class="title ellipsis2">Flink+ClickHouse 玩转企业级实时大数据开发,助你实现弯道超车</p> <p class="one"><span>进阶 · 246人报名</span></p> <p class="two clearfix"> <span class="price l red bold">¥399.00</span> <span class="add-shop-cart r"><img class="icon imv2-shopping-cart" src="../assets/cart2.svg">加购物车</span> </p> </a> </li> <li class="course-card"> <a target="_blank" href=""> <div class="img"><img src="../assets/course-5.png" alt=""></div> <p class="title ellipsis2">Flink+ClickHouse 玩转企业级实时大数据开发,助你实现弯道超车</p> <p class="one"><span>进阶 · 246人报名</span></p> <p class="two clearfix"> <span class="price l red bold">¥399.00</span> <span class="add-shop-cart r"><img class="icon imv2-shopping-cart" src="../assets/cart2.svg">加购物车</span> </p> </a> </li> </ul> <div class="page"> <span class="disabled_page">首页</span> <span class="disabled_page">上一页</span> <a href="" class="active">1</a> <a href="">2</a> <a href="">3</a> <a href="">4</a> <a href="">下一页</a> <a href="">尾页</a> </div> </div> </div> <Footer></Footer> </div> </template> <script setup> import Header from "../components/Header.vue" import Footer from "../components/Footer.vue" import course from "../api/course"; // 获取课程学习方向 course.get_course_direction().then(response=>{ course.direction_list = response.data; }) // 获取课程分类 course.get_course_category().then(response=>{ course.category_list = response.data; }) </script> <style scoped> .type .type-wrap .warp:hover{ height: auto; } .type .type-wrap .warp:hover .items{ background-color: #fff; } </style>View Code
提交代码版本
cd /home/moluo/Desktop/luffycity/ git add . git commit -m "feature: 客户端请求学习方向与课程分类列表的api接口并展示数据" git push
当用户点击不同的学习方向时,显示不同方向下的课程分类信息
服务端
调整url路由,在获取课程分类时允许地址栏传递学习方向的ID进来。
from django.urls import path,re_path from . import views urlpatterns = [ path("direction/", views.CourseDirectionListAPIView.as_view()), re_path("category/(?P<direction>\d+)/", views.CourseCategoryListAPIView.as_view()), ] 视图中获取数据时,提取路由参数作为查询的条件,courses.views,视图代码: from rest_framework.generics import ListAPIView from .models import CourseDirection, CourseCategory from .serializers import CourseDirectionModelSerializer, CourseCategoryModelSerializer # Create your views here. class CourseDirectionListAPIView(ListAPIView): """学习方向""" queryset = CourseDirection.objects.filter(is_show=True, is_delete=False).order_by("orders","id") serializer_class = CourseDirectionModelSerializer pagination_class = None class CourseCategoryListAPIView(ListAPIView): """学习分类""" serializer_class = CourseCategoryModelSerializer pagination_class = None def get_queryset(self): # 类视图中,获取路由参数 queryset = CourseCategory.objects.filter(is_show=True, is_delete=False) # 如果direction为0,则表示查询所有的课程分类,如果大于0,则表示按学习方向来查找课程分类 direction = int(self.kwargs.get("direction", 0)) if direction > 0: queryset = queryset.filter(direction=direction) return queryset.order_by("orders","id").all()View Code
客户端
在获取课程分类时,添加当前选择的学习方向的ID作为路由参数。
src/api/course.js,代码:
import http from "../utils/http"; import {reactive, ref} from "vue"; const course = reactive({ current_direction: 0, // 当前选中的学习方向,0表示所有方向 current_category: 0, // 当前选中的课程分类,0表示不限分类 direction_list: [], // 学习方向列表 category_list: [], // 课程分类列表 // 获取学习方向信息 get_course_direction(){ return http.get("/courses/directions/") }, // 获取课程分类信息 get_course_category () { return http.get(`/courses/categories/${this.current_direction}/`) } }); export default course;View Code
src/views/Course.vue,代码:
<script setup> import {watch} from "vue"; import Header from "../components/Header.vue" import Footer from "../components/Footer.vue" import course from "../api/course"; course.get_course_direction().then(response=>{ course.direction_list = response.data; }) const get_category = ()=>{ // 重置当前选中的课程分类 course.current_category=0; // 获取课程分类 course.get_course_category().then(response=>{ course.category_list = response.data; }) } get_category(); watch( // 监听当前学习方向,在改变时,更新对应方向下的课程分类 ()=> course.current_direction, ()=>{ get_category(); } ) </script>View Code
提交代码版本
cd /home/moluo/Desktop/luffycity/ git add . git commit -m "feature: 当用户点击不同的学习方向时,显示不同方向下的课程分类信息" git push
课程信息列表展示
服务端添加测试数据,luffycityapi/scripts/test_data.sql
,代码:
# 如果当前数据库使用了物理外键,需要先关闭原来表中的主外键约束功能 # set FOREIGN_KEY_CHECKS=0; # 清空原有的课程信息表信息 truncate table fg_course_info; # 添加课程信息 INSERT INTO luffycity.fg_course_info (id, name, orders, is_show, is_deleted, created_time, updated_time, course_cover, course_video, course_type, level, description, pub_date, period, attachment_path, attachment_link, status, students, lessons, pub_lessons, price, recomment_home_hot, recomment_home_top, category_id, direction_id, teacher_id) VALUES (1, '7天Typescript从入门到放弃', 1, 1, 0, '2021-07-22 04:35:05.696823', '2021-07-22 04:35:05.696871', 'course/cover/course-10.png', '', 0, 0, '<p>7天Typescript从入门到放弃</p>', '2021-07-22', 7, 'luffycity-celery用法1.zip', null, 0, 1100, 70, 15, 800.00, 0, 0, 2, 1, 1), (2, '3天Typescript精修', 1, 1, 0, '2021-07-22 04:35:05.696823', '2021-07-22 04:35:05.696871', 'course/cover/course-9.png', '', 0, 0, '<p>3天Typescript精修</p>', '2021-07-22', 7, 'luffycity-celery用法1.zip', null, 0, 9704, 100, 100, 998.00, 1, 0, 2, 1, 2), (3, '3天学会Vue基础', 1, 1, 0, '2021-07-22 04:35:05.696823', '2021-07-22 04:35:05.696871', 'course/cover/course-8.png', '', 0, 0, '<p>3天学会Vue基础</p>', '2021-07-22', 7, 'luffycity-celery用法1.zip', null, 0, 988, 130, 54, 500.00, 1, 0, 1, 1, 2), (4, '算法与数据结构体系课', 1, 1, 0, '2021-07-22 04:35:05.696823', '2021-07-22 04:35:05.696871', 'course/cover/course-7.png', '', 0, 0, '<p>算法与数据结构体系课</p>', '2021-07-22', 7, 'luffycity-celery用法1.zip', null, 0, 1303, 150, 50, 998.00, 0, 1, 33, 4, 4), (5, 'python基础入门', 1, 1, 0, '2021-07-22 04:35:05.696823', '2021-07-22 04:35:05.696871', 'course/cover/course-6.png', '', 0, 0, '<p>python基础入门</p>', '2021-07-22', 7, 'luffycity-celery用法1.zip', null, 0, 4302, 140, 30, 100.00, 0, 1, 20, 2, 4), (6, 'javascript进阶', 1, 1, 0, '2021-07-22 04:35:05.696823', '2021-07-22 04:35:05.696871', 'course/cover/course-5.png', '', 0, 0, '<p>javascript进阶</p>', '2021-07-22', 7, 'luffycity-celery用法1.zip', null, 0, 1125, 180, 100, 1750.00, 1, 0, 5, 1, 3), (7, '爬虫进阶之逆向工程', 1, 1, 0, '2021-07-22 04:35:05.696823', '2021-07-22 04:35:05.696871', 'course/cover/course-4.png', '', 0, 0, '<p>爬虫进阶之逆向工程</p>', '2021-07-22', 7, 'luffycity-celery用法1.zip', null, 0, 223, 145, 55, 400.00, 0, 0, 21, 2, 3), (8, 'Kubernetes 入门到进阶实战', 1, 1, 0, '2021-07-22 04:35:05.696823', '2021-07-22 04:35:05.696871', 'course/cover/course-3.png', '', 0, 0, '<p>Kubernetes 入门到进阶实战</p>', '2021-07-22', 7, 'luffycity-celery用法1.zip', null, 0, 6074, 70, 20, 500.00, 1, 0, 50, 7, 3), (9, 'Android 应用程序构建实战', 1, 1, 0, '2021-07-22 04:35:05.696823', '2021-07-22 04:35:05.696871', 'course/cover/course-2.png', '', 0, 0, '<p>Android 应用程序构建实战</p>', '2021-07-22', 7, 'luffycity-celery用法1.zip', null, 0, 1059, 110, 50, 550.00, 0, 0, 29, 3, 1), (10, 'Kotlin从入门到精通', 1, 1, 0, '2021-07-22 04:35:05.696823', '2021-07-22 04:35:05.696871', 'course/cover/course-1.png', '', 0, 0, '<p>Kotlin从入门到精通</p>', '2021-07-22', 7, 'luffycity-celery用法1.zip', null, 0, 870, 120, 0, 500.00, 1, 0, 29, 3, 1), (11, '深度学习之神经网络', 1, 1, 0, '2021-07-22 04:35:05.696823', '2021-07-22 04:35:05.696871', 'course/cover/course-11.png', '', 0, 0, '<p>深度学习之神经网络</p>', '2021-07-22', 7, 'luffycity-celery用法1.zip', null, 0, 6002, 115, 70, 80.00, 1, 0, 37, 5, 1), (12, 'OpenCV入门到进阶', 1, 1, 0, '2021-07-22 04:35:05.696823', '2021-07-22 04:35:05.696871', 'course/cover/course-12.png', '', 0, 0, '<p>OpenCV入门到进阶</p>', '2021-07-22', 7, 'luffycity-celery用法1.zip', null, 0, 1029, 100, 70, 390.00, 0, 1, 38, 5, 2), (13, 'Go容器化微服务系统实战', 1, 1, 0, '2021-07-22 04:35:05.696823', '2021-07-22 04:35:05.696871', 'course/cover/course-13.png', '', 0, 0, '<p>Go容器化微服务系统实战</p>', '2021-07-22', 7, 'luffycity-celery用法1.zip', null, 0, 24202, 65, 65, 399.00, 0, 0, 35, 5, 1), (14, 'RabbitMQ精讲', 1, 1, 0, '2021-07-22 04:35:05.696823', '2021-07-22 04:35:05.696871', 'course/cover/course-14.png', '', 0, 0, '<p>RabbitMQ精讲</p>', '2021-07-22', 7, 'luffycity-celery用法1.zip', null, 0, 980, 100, 100, 710.00, 0, 0, 53, 8, 4), (15, 'TensorFlow基础', 1, 1, 0, '2021-07-22 04:35:05.696823', '2021-07-22 04:35:05.696871', 'course/cover/course-15.png', '', 0, 0, '<p>RabbitMQ精讲</p>', '2021-07-22', 7, 'luffycity-celery用法1.zip', null, 0, 670, 220, 100, 1590.00, 0, 1, 36, 5, 2), (16, 'ZooKeeper分布式架构搭建', 1, 1, 0, '2021-07-22 04:35:05.696823', '2021-07-22 04:35:05.696871', 'course/cover/course-16.png', '', 0, 0, '<p>ZooKeeper分布式架构搭建</p>', '2021-07-22', 7, 'luffycity-celery用法1.zip', null, 0, 90, 88, 35, 40.00, 1, 0, 35, 5, 3), (17, '高性能MySQL调优', 1, 1, 0, '2021-07-22 04:35:05.696823', '2021-07-22 04:35:05.696871', 'course/cover/course-17.png', '', 0, 0, '<p>高性能MySQL调优</p>', '2021-07-22', 7, 'luffycity-celery用法1.zip', null, 0, 40, 300, 60, 998.00, 1, 1, 60, 10, 3), (18, 'MySQL事务处理精选', 1, 1, 0, '2021-07-22 04:35:05.696823', '2021-07-22 04:35:05.696871', 'course/cover/course-18.png', '', 0, 0, '<p>MySQL事务处理精选</p>', '2021-07-22', 7, 'luffycity-celery用法1.zip', null, 0, 640, 65, 30, 1000.00, 1, 0, 60, 10, 1), (19, 'MongoDB入门到进阶', 1, 1, 0, '2021-07-22 04:35:05.696823', '2021-07-22 04:35:05.696871', 'course/cover/course-19.png', '', 0, 0, '<p>MongoDB入门到进阶</p>', '2021-07-22', 7, 'luffycity-celery用法1.zip', null, 0, 11205, 86, 40, 1100.00, 0, 1, 62, 10, 3), (20, 'Redis入门课程', 1, 1, 0, '2021-07-22 04:35:05.696823', '2021-07-22 04:35:05.696871', 'course/cover/course-20.png', '', 0, 0, '<p>Redis入门课程</p>', '2021-07-22', 7, 'luffycity-celery用法1.zip', null, 0, 120, 100, 40, 1199.00, 1, 1, 61, 10, 2); # 如果当前数据库使用了物理外键,开启原来表中的主外键约束功能 # set FOREIGN_KEY_CHECKS=1;View Code
终端下执行shell文件
cd /home/moluo/Desktop/luffycity/luffycityapi/scripts ./test_data.sh
服务端
提供课程信息列表的api接口
序列化器,courses.serializers
,代码:
from .models import Course class CourseInfoModelSerializer(serializers.ModelSerializer): """课程信息的序列化器""" class Meta: model = Course fields = [ "id", "name", "course_cover", "level", "get_level_display", "students", "status", "get_status_display", "lessons", "pub_lessons", "price", "discount" ]View Code
模型,courses/models.py
,代码:
class Course(BaseModel): course_type = ( (0, '付费购买'), (1, '会员专享'), (2, '学位课程'), ) level_choices = ( (0, '初级'), (1, '中级'), (2, '高级'), ) status_choices = ( (0, '上线'), (1, '下线'), (2, '预上线'), ) # course_cover = models.ImageField(upload_to="course/cover", max_length=255, verbose_name="封面图片", blank=True, null=True) course_cover = StdImageField(variations={ 'thumb_1080x608': (1080, 608), # 高清图 'thumb_540x304': (540, 304), # 中等比例, 'thumb_108x61': (108, 61, True), # 小图(第三个参数表示保持图片质量), }, max_length=255, delete_orphans=True, upload_to="course/cover", null=True, verbose_name="封面图片",blank=True) course_video = models.FileField(upload_to="course/video", max_length=255, verbose_name="封面视频", blank=True, null=True) course_type = models.SmallIntegerField(choices=course_type,default=0, verbose_name="付费类型") level = models.SmallIntegerField(choices=level_choices, default=1, verbose_name="难度等级") description = RichTextUploadingField(null=True, blank=True, verbose_name="详情介绍") pub_date = models.DateField(auto_now_add=True, verbose_name="发布日期") period = models.IntegerField(default=7, verbose_name="建议学习周期(day)") attachment_path = models.FileField(max_length=1000, blank=True, null=True, verbose_name="课件路径") attachment_link = models.CharField(max_length=1000, blank=True, null=True, verbose_name="课件链接") status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="课程状态") students = models.IntegerField(default=0, verbose_name="学习人数") lessons = models.IntegerField(default=0, verbose_name="总课时数量") pub_lessons = models.IntegerField(default=0, verbose_name="已更新课时数量") price = models.DecimalField(max_digits=10,decimal_places=2, verbose_name="课程原价",default=0) recomment_home_hot = models.BooleanField(default=False, verbose_name="是否推荐到首页新课栏目") recomment_home_top = models.BooleanField(default=False, verbose_name="是否推荐到首页必学栏目") direction = models.ForeignKey("CourseDirection", related_name="course_list", on_delete=models.DO_NOTHING, null=True, blank=True, db_constraint=False, verbose_name="学习方向") category = models.ForeignKey("CourseCategory", related_name="course_list", on_delete=models.DO_NOTHING, null=True, blank=True, db_constraint=False, verbose_name="课程分类") teacher = models.ForeignKey("Teacher", related_name="course_list", on_delete=models.DO_NOTHING, null=True, blank=True, db_constraint=False, verbose_name="授课老师") class Meta: db_table = "fg_course_info" verbose_name = "课程信息" verbose_name_plural = verbose_name def __str__(self): return "%s" % self.name def course_cover_small(self): if self.course_cover: return mark_safe(f'<img style="border-radius: 0%;" src="{self.course_cover.thumb_108x61.url}">') return "" course_cover_small.short_description = "封面图片(108x61)" course_cover_small.allow_tags = True course_cover_small.admin_order_field = "course_cover" def course_cover_medium(self): if self.course_cover: return mark_safe(f'<img style="border-radius: 0%;" src="{self.course_cover.thumb_540x304.url}">') return "" course_cover_medium.short_description = "封面图片(540x304)" course_cover_medium.allow_tags = True course_cover_medium.admin_order_field = "course_cover" def course_cover_large(self): if self.course_cover: return mark_safe(f'<img style="border-radius: 0%;" src="{self.course_cover.thumb_1080x608.url}">') return "" course_cover_large.short_description = "封面图片(1080x608)" course_cover_large.allow_tags = True course_cover_large.admin_order_field = "course_cover" @property def discount(self): # todo 将来通过计算获取当前课程的折扣优惠相关的信息 import random return { "type": ["限时优惠","限时减免"].pop(random.randint(0,1)), # 优惠类型 "expire": random.randint(100000, 1200000), # 优惠倒计时 "price": self.price - random.randint(1,10) * 10, # 优惠价格 } View Code
视图,courses/views.py
,代码:
from rest_framework.generics import ListAPIView from .models import CourseDirection, CourseCategory, Course from .serializers import CourseDirectionModelSerializer, CourseCategoryModelSerializer, CourseInfoModelSerializer # Create your views here. class CourseDirectionListAPIView(ListAPIView): """学习方向""" queryset = CourseDirection.objects.filter(is_show=True, is_deleted=False).order_by("orders", "-id") serializer_class = CourseDirectionModelSerializer pagination_class = None class CourseCategoryListAPIView(ListAPIView): """学习分类""" # queryset = CourseCategory.objects.filter(is_show=True, is_deleted=False).order_by("orders","-id") serializer_class = CourseCategoryModelSerializer pagination_class = None def get_queryset(self): # 类视图中,获取路由参数 queryset = CourseCategory.objects.filter(is_show=True, is_deleted=False) # 如果direction为0,则表示查询所有的课程分类,如果大于0,则表示按学习方向来查找课程分类 direction = int(self.kwargs.get("direction", 0)) if direction > 0: queryset = queryset.filter(direction=direction) return queryset.order_by("orders", "id").all() # url: /course/学习方向ID/课程分类 # url: /course/P<direction>\d+)/(?P<category>\d+)$/ # url: /course/0/0 # 展示所有的课程列表信息,不区分学习方向和课程分类 # url: /course/1/0 # 展示前端开发学习方向的课程列表信息,不区分课程分类 # url: /course/1/5 # 展示前端开发学习方向下javascript课程分类的课程列表信息 class CourseListAPIView(ListAPIView): """课程列表接口""" serializer_class = CourseInfoModelSerializer def get_queryset(self): queryset = Course.objects.filter(is_deleted=False, is_show=True).order_by("-orders", "-id") direction = int(self.kwargs.get("direction", 0)) category = int(self.kwargs.get("category", 0)) # 只有在学习方向大于0的情况下才进行学习方向的过滤 if direction > 0: queryset = queryset.filter(direction=direction) # 只有在课程分类大于0的情况下才进行课程分类的过滤 if category > 0: queryset = queryset.filter(category=category) return queryset.all()View Code
courses.urls,路由:
from django.urls import path, re_path from . import views urlpatterns = [ path("directions/", views.CourseDirectionListAPIView.as_view()), re_path(r"categories/(?P<direction>\d+)/", views.CourseCategoryListAPIView.as_view()), re_path(r"^(?P<direction>\d+)/(?P<category>\d+)/$", views.CourseListAPIView.as_view()), ]View Code
客户端
发送请求获取课程列表信息
src/api/course.js,代码:
import http from "../utils/http"; import {reactive, ref} from "vue" const course = reactive({ current_direction: 0, // 当前选中的学习方向,0表示所有方向 current_category: 0, // 当前选中的课程分类,0表示不限分类 direction_list: [], // 学习方向列表 category_list: [], // 课程分类列表 course_list: [], // 课程列表数据 get_course_direction(){ // 获取学习方向信息 return http.get("/courses/directions/") }, get_course_category () { // 获取课程分类信息 return http.get(`/courses/categories/${this.current_direction}/`) }, get_course_list () { // 获取课程列表信息 return http.get(`/courses/${this.current_direction}/${this.current_category}/`) } }) export default course;View Code
Course.vue,代码:
<template> <div class="course"> <Header></Header> <div class="top-wrap"> <div class="actual-header"> <div class="actual-header-wrap"> <div class="banner"> <router-link class="title" to="/course"><img class="h100" src="../assets/coding-title.png" alt=""></router-link> <div>真实项目实战演练</div> </div> <div class="actual-header-search"> <div class="search-inner"> <input class="actual-search-input" placeholder="搜索感兴趣的实战课程内容" type="text" autocomplete="off"> <img class="actual-search-button" src="../assets/search.svg" /> </div> <div class="actual-searchtags"> </div> <div class="search-hot"> <span>热搜:</span> <a href="">Java工程师</a> <a href="">Vue</a> </div> </div> </div> </div> <div class="type"> <div class="type-wrap"> <div class="one warp"> <span class="name">方向:</span> <ul class="items"> <li :class="{cur:course.current_direction===0}" @click.prevent.stop="course.current_direction=0"><a href="">全部</a></li> <li :class="{cur:course.current_direction===direction.id}" @click.prevent.stop="course.current_direction=direction.id" v-for="direction in course.direction_list"><a href="">{{direction.name}}</a></li> </ul> </div> <div class="two warp"> <span class="name">分类:</span> <ul class="items"> <li :class="{cur:course.current_category===0}"><a href="" @click.prevent.stop="course.current_category=0">不限</a></li> <li :class="{cur:course.current_category===category.id}" v-for="category in course.category_list"><a href="" @click.prevent.stop="course.current_category=category.id">{{category.name}}</a></li> </ul> </div> </div> </div> </div> <div class="main"> <div class="main-wrap"> <div class="filter clearfix"> <div class="sort l"> <a href="" class="on">最新</a> <a href="">销量</a> <a href="">升级</a> </div> <div class="other r clearfix"><a class="course-line l" href="" target="_blank">学习路线</a></div> </div> <ul class="course-list clearfix"> <li class="course-card" v-for="course_info in course.course_list"> <a target="_blank" href=""> <div class="img"><img :src="course_info.course_cover" alt=""></div> <p class="title ellipsis2">{{course_info.name}}</p> <p class="one"> <span>{{course_info.get_level_display}} · {{course_info.students}}人报名</span> <span class="discount r"> <i class="name" v-if="course_info.discount.type">{{course_info.discount.type}}</i> <i class="countdown" v-if="course_info.discount.expire">{{parseInt(course_info.discount.expire/86400)}}<span class="day">天</span>{{fill0(parseInt(course_info.discount.expire/3600%24))}}:{{fill0(parseInt(course_info.discount.expire/60%60))}}:{{fill0(parseInt(course_info.discount.expire%60))}}</i> </span> </p> <p class="two clearfix"> <span class="price l red bold" v-if="course_info.discount.price">¥{{parseFloat(course_info.discount.price).toFixed(2)}}</span> <span class="price l red bold" v-else>¥{{parseFloat(course_info.price).toFixed(2)}}</span> <span class="origin-price l delete-line" v-if="course_info.discount.price">¥{{parseFloat(course_info.price).toFixed(2)}}</span> <span class="add-shop-cart r"><img class="icon imv2-shopping-cart" src="../assets/cart2.svg">加购物车</span> </p> </a> </li> <!-- <li class="course-card">--> <!-- <a target="_blank" href="">--> <!-- <div class="img"><img src="../assets/course-2.png" alt=""></div>--> <!-- <p class="title ellipsis2">Flink+ClickHouse 玩转企业级实时大数据开发,助你实现弯道超车</p>--> <!-- <p class="one">--> <!-- <span>进阶 · 246人报名</span>--> <!-- <span class="discount r"><i class="name">限时优惠</i><i class="countdown">6<span class="day">天</span>01:39:21</i></span>--> <!-- </p>--> <!-- <p class="two clearfix">--> <!-- <span class="price l red bold">¥328.00</span>--> <!-- <span class="origin-price l delete-line">¥368.00</span>--> <!-- <span class="add-shop-cart r"><img class="icon imv2-shopping-cart" src="../assets/cart2.svg">加购物车</span>--> <!-- </p>--> <!-- </a>--> <!-- </li>--> <!-- <li class="course-card">--> <!-- <a target="_blank" href="">--> <!-- <div class="img"><img src="../assets/course-3.png" alt=""></div>--> <!-- <p class="title ellipsis2">Flink+ClickHouse 玩转企业级实时大数据开发,助你实现弯道超车</p>--> <!-- <p class="one">--> <!-- <span>进阶 · 246人报名</span>--> <!-- <span class="discount r"><i class="name">限时优惠</i><i class="countdown">16<span class="day">天</span>01:39:21</i></span>--> <!-- </p>--> <!-- <p class="two clearfix">--> <!-- <span class="price l red bold">¥328.00</span>--> <!-- <span class="origin-price l delete-line">¥368.00</span>--> <!-- <span class="add-shop-cart r"><img class="icon imv2-shopping-cart" src="../assets/cart2.svg">加购物车</span>--> <!-- </p>--> <!-- </a>--> <!-- </li>--> <!-- <li class="course-card">--> <!-- <a target="_blank" href="">--> <!-- <div class="img"><img src="../assets/course-4.png" alt=""></div>--> <!-- <p class="title ellipsis2">Flink+ClickHouse 玩转企业级实时大数据开发,助你实现弯道超车</p>--> <!-- <p class="one"><span>进阶 · 246人报名</span></p>--> <!-- <p class="two clearfix">--> <!-- <span class="price l red bold">¥399.00</span>--> <!-- <span class="add-shop-cart r"><img class="icon imv2-shopping-cart" src="../assets/cart2.svg">加购物车</span>--> <!-- </p>--> <!-- </a>--> <!-- </li>--> <!-- <li class="course-card">--> <!-- <a target="_blank" href="">--> <!-- <div class="img"><img src="../assets/course-5.png" alt=""></div>--> <!-- <p class="title ellipsis2">Flink+ClickHouse 玩转企业级实时大数据开发,助你实现弯道超车</p>--> <!-- <p class="one"><span>进阶 · 246人报名</span></p>--> <!-- <p class="two clearfix">--> <!-- <span class="price l red bold">¥399.00</span>--> <!-- <span class="add-shop-cart r"><img class="icon imv2-shopping-cart" src="../assets/cart2.svg">加购物车</span>--> <!-- </p>--> <!-- </a>--> <!-- </li>--> </ul> <div class="page"> <span class="disabled_page">首页</span> <span class="disabled_page">上一页</span> <a href="" class="active">1</a> <a href="">2</a> <a href="">3</a> <a href="">4</a> <a href="">下一页</a> <a href="">尾页</a> </div> </div> </div> <Footer></Footer> </div> </template> <script setup> import {reactive,ref, watch} from "vue" import Header from "../components/Header.vue" import Footer from "../components/Footer.vue" import course from "../api/course"; import {fill0} from "../utils/func"; // 获取学习方向的列表数据 course.get_course_direction().then(response=>{ course.direction_list = response.data; }) // 获取课程分类的列表数据 const get_category = ()=>{ // 获取课程分类 course.get_course_category().then(response=>{ course.category_list = response.data; }) } get_category(); const get_course_list = ()=>{ // 获取课程列表 course.get_course_list().then(response=>{ course.course_list = response.data; }) } get_course_list(); watch( // 监听当前学习方向,在改变时,更新对应方向下的课程分类与课程信息 ()=> course.current_direction, ()=>{ // 重置当前选中的课程分类 course.current_category=0; get_category(); get_course_list(); } ) watch( // 监听切换不同的课程分类,在改变时,更新对应分类下的课程信息 ()=> course.current_category, ()=>{ get_course_list(); } ) </script>View Code
src/utils/func.js,代码:
// 给小于10的数字左边补0 export function fill0(num){ return num<10?"0"+num: num; }View Code
提交代码版本
cd /home/moluo/Desktop/luffycity/ git add . git commit -m "feature: 根据不同的学习方向与课程分类,展示课程列表信息" git push
排序展示课程信息
后端提供排序课程的接口,
只需要在courses/views.py
把原来views.py中的CoursesAPIView新增两句代码:
from rest_framework.generics import ListAPIView from .models import CourseDirection, CourseCategory, Course from .serializers import CourseDirectionModelSerializer, CourseCategoryModelSerializer, CourseInfoModelSerializer from rest_framework.filters import OrderingFilter # 中间代码省略... # url: /course/学习方向ID/课程分类 # url: /course/P<direction>\d+)/(?P<category>\d+)$/ # url: /course/0/0 # 展示所有的课程列表信息,不区分学习方向和课程分类 # url: /course/1/0 # 展示前端开发学习方向的课程列表信息,不区分课程分类 # url: /course/1/5 # 展示前端开发学习方向下javascript课程分类的课程列表信息 class CourseListAPIView(ListAPIView): """课程列表接口""" serializer_class = CourseInfoModelSerializer # 新增两句代码 filter_backends = [OrderingFilter, ] ordering_fields = ['id', 'students', 'orders'] def get_queryset(self): queryset = Course.objects.filter(is_deleted=False, is_show=True).order_by("-orders", "-id") direction = int(self.kwargs.get("direction", 0)) category = int(self.kwargs.get("category", 0)) # 只有在学习方向大于0的情况下才进行学习方向的过滤 if direction > 0: queryset = queryset.filter(direction=direction) # 只有在课程分类大于0的情况下才进行课程分类的过滤 if category > 0: queryset = queryset.filter(category=category) return queryset.all() View Code
客户端根据排序字段对应的课程顺序
src/api/course.js,代码:
import http from "../utils/http"; import {reactive, ref} from "vue" const course = reactive({ current_direction: 0, // 当前选中的学习方向,0表示所有方向 current_category: 0, // 当前选中的课程分类,0表示不限分类 direction_list: [], // 学习方向列表 category_list: [], // 课程分类列表 course_list: [], // 课程列表数据 ordering: "-id", // 课程排序条件 get_course_direction(){ // 获取学习方向信息 return http.get("/courses/directions/") }, get_course_category () { // 获取课程分类信息 return http.get(`/courses/categories/${this.current_direction}/`) }, get_course_list () { // 获取课程列表信息 let params = {} if(this.ordering){ params.ordering = this.ordering; } return http.get(`/courses/${this.current_direction}/${this.current_category}/`, { params, // params: params 的简写 }) } }) export default course;View Code
views/Course.vue,代码
<div class="filter clearfix"> <div class="sort l"> <a href="" :class="{on:course.ordering==='-id'}" @click.prevent.stop="course.ordering=(course.ordering==='-id'?'':'-id')">最新</a> <a href="" :class="{on:course.ordering==='-students'}" @click.prevent.stop="course.ordering=(course.ordering==='-students'?'':'-students')">销量</a> <a href="" :class="{on:course.ordering==='-orders'}" @click.prevent.stop="course.ordering=(course.ordering==='-orders'?'':'-orders')">推荐</a> </div> <div class="other r clearfix"><a class="course-line l" href="" target="_blank">学习路线</a></div> </div> <script setup> import {reactive,ref, watch} from "vue" import Header from "../components/Header.vue" import Footer from "../components/Footer.vue" import course from "../api/course"; import {fill0} from "../utils/func"; // 获取学习方向的列表数据 course.get_course_direction().then(response=>{ course.direction_list = response.data; }) // 获取课程分类的列表数据 const get_category = ()=>{ // 获取课程分类 course.get_course_category().then(response=>{ course.category_list = response.data; }) } get_category(); const get_course_list = ()=>{ // 获取课程列表 course.get_course_list().then(response=>{ course.course_list = response.data; }) } get_course_list(); watch( // 监听当前学习方向,在改变时,更新对应方向下的课程分类与课程信息 ()=> course.current_direction, ()=>{ // 重置排序条件 course.ordering = "-id"; // 重置当前选中的课程分类 course.current_category=0; get_category(); get_course_list(); } ) watch( // 监听切换不同的课程分类,在改变时,更新对应分类下的课程信息 ()=> course.current_category, ()=>{ // 重置排序条件 course.ordering = "-id"; get_course_list(); } ) watch( // 监听课程切换不同的排序条件 ()=>course.ordering, ()=>{ get_course_list(); } ) </script>View Code
分页展示课程信息
服务端
调整课程信息的api接口,实现分页查询。
courses.paginations,代码:
from rest_framework.pagination import PageNumberPagination class CourseListPageNumberPagination(PageNumberPagination): """课程信息列表分页器""" page_size = 5 max_page_size = 20 page_size_query_param = "size" page_query_param = "page" View Code
courses.views,代码:
from rest_framework.generics import ListAPIView from .models import CourseDirection, CourseCategory, Course from .serializers import CourseDirectionModelSerializer, CourseCategoryModelSerializer, CourseInfoModelSerializer from rest_framework.filters import OrderingFilter from .paginations import CourseListPageNumberPagination # url: /course/学习方向ID/课程分类 # url: /course/P<direction>\d+)/(?P<category>\d+)$/ # url: /course/0/0 # 展示所有的课程列表信息,不区分学习方向和课程分类 # url: /course/1/0 # 展示前端开发学习方向的课程列表信息,不区分课程分类 # url: /course/1/5 # 展示前端开发学习方向下javascript课程分类的课程列表信息 class CourseListAPIView(ListAPIView): """课程列表接口""" serializer_class = CourseInfoModelSerializer filter_backends = [OrderingFilter, ] ordering_fields = ['id', 'students', 'orders'] pagination_class = CourseListPageNumberPagination def get_queryset(self): queryset = Course.objects.filter(is_deleted=False, is_show=True).order_by("-orders", "-id") direction = int(self.kwargs.get("direction", 0)) category = int(self.kwargs.get("category", 0)) # 只有在学习方向大于0的情况下才进行学习方向的过滤 if direction > 0: queryset = queryset.filter(direction=direction) # 只有在课程分类大于0的情况下才进行课程分类的过滤 if category > 0: queryset = queryset.filter(category=category) return queryset.all() View Code
客户端
因为服务端改成分页展示数据,所以返回的数据的结构发生了改变,而且需要根据数据量来决定是否展示分页或者展示分页页码
api/course.js,代码:
import http from "../utils/http"; import {reactive, ref} from "vue" const course = reactive({ current_direction: 0, // 当前选中的学习方向,0表示所有方向 current_category: 0, // 当前选中的课程分类,0表示不限分类 direction_list: [], // 学习方向列表 category_list: [], // 课程分类列表 course_list: [], // 课程列表数据 ordering: "", // 课程排序条件 page: 1, // 当前页码,默认为1 size: 5, // 当前页数据量 count: 0, // 课程信息列表的数量 has_perv: false, // 是否有上一页 has_next: false, // 是否有下一页 timer: null, // 课程相关数据的定时器 // 获取学习方向信息 get_course_direction(){ return http.get("/courses/direction") }, // 获取课程分类信息 get_course_category () { return http.get(`/courses/category/${this.current_direction}/`,) }, // 获取课程列表信息 get_course_list () { let params = { page: this.page, size: this.size, } if(this.ordering){ params.ordering = this.ordering; } return http.get(`/courses/${this.current_direction}/${this.current_category}/`,{ params, }) } }) export default course;View Code
views/Course.vue,代码:
<div class="page" v-if="course.count > course.size"> <a href="" v-if="course.has_perv" @click.prevent.stop="course.page=1">首页</a> <span v-else>首页</span> <a href="" v-if="course.has_perv" @click.prevent.stop="course.page--">上一页</a> <span v-else>上一页</span> <a href="" v-if="course.has_perv" @click.prevent.stop="course.page--">{{course.page-1}}</a> <a class="active">{{course.page}}</a> <a href="" v-if="course.has_next" @click.prevent.stop="course.page++">{{course.page+1}}</a> <a href="" v-if="course.has_next" @click.prevent.stop="course.page++">下一页</a> <span v-else>下一页</span> <a href="" v-if="course.has_next" @click.prevent.stop="course.page=Math.ceil(course.count/course.size)">尾页</a> <span v-else>尾页</span> </div> <script setup> import {reactive,ref, watch} from "vue" import Header from "../components/Header.vue" import Footer from "../components/Footer.vue" import course from "../api/course"; import {fill0} from "../utils/func"; // 获取学习方向的列表数据 course.get_course_direction().then(response=>{ course.direction_list = response.data; }) // 获取课程分类的列表数据 const get_category = ()=>{ // 获取课程分类 course.get_course_category().then(response=>{ course.category_list = response.data; }) } get_category(); const get_course_list = ()=>{ // 获取课程列表 course.get_course_list().then(response=>{ course.course_list = response.data.results; // 总数据量 course.count = response.data.count; course.has_perv = !!response.data.previous; // !!2个非表示把数据转换成布尔值 course.has_next = !!response.data.next; }) } get_course_list(); watch( // 监听当前学习方向,在改变时,更新对应方向下的课程分类与课程信息 ()=> course.current_direction, ()=>{ // 重置排序条件 course.ordering = "-id"; // 重置当前选中的课程分类 course.current_category=0; get_category(); get_course_list(); } ) watch( // 监听切换不同的课程分类,在改变时,更新对应分类下的课程信息 ()=> course.current_category, ()=>{ // 重置排序条件 course.ordering = "-id"; get_course_list(); } ) watch( // 监听课程切换不同的排序条件 ()=>course.ordering, ()=>{ get_course_list(); } ) // 监听页码 watch( ()=>course.page, ()=>{ // 重新获取课程信息 get_course_list(); } ) </script>View Code
使用计时器让活动时间不断减少
api/courses.js,代码:
import http from "../utils/http"; import {reactive, ref} from "vue" const course = reactive({ current_direction: 0, // 当前选中的学习方向,0表示所有方向 current_category: 0, // 当前选中的课程分类,0表示不限分类 direction_list: [], // 学习方向列表 category_list: [], // 课程分类列表 course_list: [], // 课程列表数据 ordering: "-id", // 课程排序条件 page: 1, // 当前页码,默认为1 size: 5, // 当前页数据量 count: 0, // 课程信息列表的数量 has_perv: false, // 是否有上一页 has_next: false, // 是否有下一页 timer: 0, // 课程相关数据的定时器 get_course_direction(){ // 获取学习方向信息 return http.get("/courses/directions/") }, get_course_category () { // 获取课程分类信息 return http.get(`/courses/categories/${this.current_direction}/`) }, get_course_list () { // 获取课程列表信息 let params = { page: this.page, size: this.size, } if(this.ordering){ params.ordering = this.ordering; } return http.get(`/courses/${this.current_direction}/${this.current_category}/`, { params, // params: params 的简写 }) }, start_timer () { // 课程相关的优惠活动倒计时 clearInterval(this.timer); // 保证整个页面只有一个倒计时对优惠活动的倒计时进行时间 this.timer = setInterval(() => { this.course_list.forEach((course) => { // js的对象和python里面的字典/列表一样, 是属于引用类型的。所以修改了成员的值也会影响自身的。 if (course.discount.expire && course.discount.expire > 0) { // 时间不断自减 course.discount.expire-- } }) }, 1000) } }) export default course;View Code
views/Course.vue,代码:
<script setup> import {reactive,ref, watch} from "vue" import Header from "../components/Header.vue" import Footer from "../components/Footer.vue" import course from "../api/course"; import {fill0} from "../utils/func"; // 获取学习方向的列表数据 course.get_course_direction().then(response=>{ course.direction_list = response.data; }) // 获取课程分类的列表数据 const get_category = ()=>{ // 获取课程分类 course.get_course_category().then(response=>{ course.category_list = response.data; }) } get_category(); const get_course_list = ()=>{ // 获取课程列表 course.get_course_list().then(response=>{ course.course_list = response.data.results; // 总数据量 course.count = response.data.count; course.has_perv = !!response.data.previous; // !!2个非表示把数据转换成布尔值 course.has_next = !!response.data.next; // 优惠活动的倒计时 course.start_timer(); }) } get_course_list(); watch( // 监听当前学习方向,在改变时,更新对应方向下的课程分类与课程信息 ()=> course.current_direction, ()=>{ // 重置排序条件 course.ordering = "-id"; // 重置当前选中的课程分类 course.current_category=0; get_category(); get_course_list(); } ) watch( // 监听切换不同的课程分类,在改变时,更新对应分类下的课程信息 ()=> course.current_category, ()=>{ // 重置排序条件 course.ordering = "-id"; get_course_list(); } ) watch( // 监听课程切换不同的排序条件 ()=>course.ordering, ()=>{ get_course_list(); } ) // 监听页码 watch( ()=>course.page, ()=>{ // 重新获取课程信息 get_course_list(); } ) </script>View Code
提交代码版本
cd /home/moluo/Desktop/luffycity/ git add . git commit -m "feature: 课程列表实现排序展示、分页展示和活动时间倒计时" git push
标签:07,verbose,Admin,name,course,models,2021,前台 From: https://www.cnblogs.com/erhuoyuan/p/16810947.html