课程管理模块-课程列表(前台和Admin)
课程列表页
前端显示课程列表页面

<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>
注册路由,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 router
将素材图片复制到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
# 推送到远程master
git push origin master
# 删除分支 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', ]
注册子路由,course/urls,代码:

from django.urls import path,re_path from . import views urlpatterns = [ ]
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")), ]
数据模型创建
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)
执行数据迁移
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运营站点 # ... ]
settings/dev(修改):(admin站点内的转换成汉语显示)

LANGUAGE_CDDE = "zh-hans" TIME_ZONE = "Asia/Shanghai"
把当前新增的课程的相关模型注册到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_name
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)
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)
给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, }, }
自定义工具条的名称:
总路由,luffycityapi.urls,代码:

urlpatterns = [ # ... path('ckeditor/', include('ckeditor_uploader.urls')), # ... ]
经过上面的配置以后,我们就可以让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)
因为这个字段属于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)
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"
提交版本
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/'
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, ]
给图片字段生成缩略图
在项目开发中,经常会遇到需要以图片的方式来展示商品/课程/物品/人物,但是如果每次展示的图片都是高清图片,则客户端访问页面时,下载这个图片就占据我们服务端的一定的网络资源,因为高清图片往往比较大,也可以影响用户访问页面的速度。让项目在列表中展示缩略图即可,真正的高清图直接在详情页中展示(例如京东商城)。
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', ]
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"
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)