1. Django缓存到redis中

django_redis模块.(继承django的cache, 使用方式进而cache的使用一致, 数据被加密存到redis中)
* 1. pip install django_redis
* 2. 在settings/.py 配置文件中配置django_redis的信息
# CACHES 是Django的缓存配置
CACHES = {# 默认配置, 可以配置多个"default": {# Django缓存使用django_redis"BACKEND": "django_redis.cache.RedisCache",# redis的地址"LOCATION": "redis://127.0.0.1:6379",# 选项"OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient",# 连接池最大100个"CONNECTION_POOL_KWARGS": {"max_connections": 100}# 密码, 目前没有设置mum# "PASSWORD": "xxx",}}}
配置之后, 短信验证码会保存到redis中.

文件夹是自动创建的...

2. 缓存轮播图

修改轮播图接口, 优先从redis中获取用户的数据, 如果redis中不存在, 再去数据库中获取数据. 获取之后保存
一份到redis中.视图类继承ListAPIView类, 路由匹配成功会自动调用视图类的list方法.
原来接口使用的内置的list方法获取轮播图的数据, 自定义list方法, 先充redis中获取数据, 如果获取到了, 使用自定义的response返回数据.
没有获取到数据,调用父类的list方法, 去数据库中读取数据,
返回response对象, 从response.data中获取数据, 使用自定义的response返回数据.
# 1. 轮播图接口# 导入ListAPIView (继承GenericAPIView, ListModelMixin
from rest_framework.generics import ListAPIView
# 导入模型层
from . import models
# 导入序列化器模块, 自定义的响应对象
from utils import Serializer, api_response# 导入配置文件, 先从dev配置文件中加在数据, 如果没有会去内置的配置文件中找
from django.conf import settings# 导入cache
from django.core.cache import cache# 导入模型
class CarouselAPI(ListAPIView):# 设置queryset属性值 过滤没有删除 要显示的数据, 查询之后按display_order排序, 在通过索取值取指定的数量queryset = models.CarouselModel.objects.filter(is_delete=False, is_show=True).order_by('display_order')[:settings.CAROUSEL_SHOW_QUANTITY]# 设置使用的模型序列化器serializer_class = Serializer.CarouselModelSerializerdef list(self, request, *args, **kwargs):# 直接去redis中获取数据, 如果没有在去数据库中取, 数据库中取出之后redis中保存一份carousel_list = cache.get('Carousel_list')if carousel_list:return api_response.ResponseDataFormat(data=carousel_list)print('走数据库!')  # 测试之后注释掉res = super().list(request, *args, **kwargs)# 设置超时时间cache.set('Carousel_list', res.data, 60 * 60 * 24)return api_response.ResponseDataFormat(data=res.data)
第一次获取数据redis中肯定没有数据, 去数据库中拿, 之后在数据没有过期的情况下都会去redis中获取.

3. 定时更新录播图缓存

* 1. 安装celery模块pip install celery==4.4.6
* 2. 安装eventlet( windows 需要下载eventlet 模块 )pip install eventlet
* 3. 在项目根目录下新建celert_task包.
* 4. 在celery_task在新建celery.py 文件(名称固定).
* 5. 在celery.py中创建定时任务
# celery_task是一个单独的项目, 这个项目需要使用django内的模块需要先加载django的环境
# 导入django模块
import django
# 导入os模块
import os# django使用的配置文件, luffy/settings/dev
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffy.settings.dev')
# 配置设置
django.setup()# ----------------------------------------------------------------
# 导入Celery类
from celery import Celery
# 任务的定时配置
from datetime import timedelta# 定义任务中间件存放地址, 需要从这个地址中取出任务
broker = 'redis://127.0.0.1:6379/1'  # 使用db1
# 定义任务结果仓库, 任务执之后的结果的保存地址
backend = 'redis://127.0.0.1:6379/2'  # # 使用db2# redis://... 是redis定义的协议.# 生成一个app对象 main=__name__, 设置一个名字
app = Celery(__name__, broker=broker, backend=backend, include=['celery_task.home_task'])# 时区 亚洲上海  app.conf.timezone 与 app.conf.enable_utc是成对设置的
app.conf.timezone = 'Asia/Shanghai'
# 是否使用UTC
app.conf.enable_utc = Falseapp.conf.beat_schedule = {# 定义一个名字'carousel_update': {# 任务: 任务地址'task': 'celery_task.home_task.carousel_update',# 定时 60 * 60 秒执行一次'schedule': timedelta(seconds=60 * 60),# 'schedule': crontab(hour=8, day_of_week=1),  # 每周一早八点# 任务参数# 'args': (1, 2),}
}
* 6. 启动worker端, hang住等待任务...

PS F:\synchro\Project\luffy>
celery worker -A celery_task -l info -P eventlet
* 7. 在celery_task包目录下新建home_task.py 文件, 首页轮播图任务
# 导入celery对象
from .celery import app# 从数据库中获取轮播图的数据
@app.task
def carousel_update():# 到模块在函数内导, 路径越短越好, 为了避免循环导入# 导入模型层from home import models# 导入模型序列化类from utils.Serializer import CarouselModelSerializer# 导入配置文件from django.conf import settings# 从数据库中获取queryset对象queryset = models.CarouselModel.objects.filter(is_delete=False, is_show=True).order_by('display_order')[:settings.CAROUSEL_SHOW_QUANTITY]# 将queryset对象序列化ser = CarouselModelSerializer(instance=queryset, many=True)print(ser.data)
* 8. 启动beat端
celery beat -A celery_task -l info
添加一次任务后, ctrl + c 终止beat端

* 9. worker端查看结果

[OrderedDict([('name', 'Carouse1'), ('link', 'free-course'), ('img', '/media/Carousel/Carouse1.png')]),
OrderedDict([('name', 'Carouse2'), ('link', 'actual-course'), ('img', '/media/Carousel/Carouse2.png')]),
OrderedDict([('name', 'Carouse3'), ('link', 'light-course'), ('img', '/media/Carousel/Carouse3.png')])]

* 10. 手动拼接路径
在视图类中获取的数据会或request对象中取出地址拼接, 而在这里没有request对象不会自动地址, 需要手动加.
# 导入celery对象
from .celery import app# 从数据库中获取轮播图的数据
@app.task
def carousel_update():# 到模块在函数内导, 路径越短越好, 为了避免循环导入# 导入模型层from home import models# 导入模型序列化类from utils.Serializer import CarouselModelSerializer# 导入配置文件from django.conf import settings# 导入cachefrom django.core.cache import cache# 从数据库中获取queryset对象queryset = models.CarouselModel.objects.filter(is_delete=False, is_show=True).order_by('display_order')[:settings.CAROUSEL_SHOW_QUANTITY]# 将queryset对象序列化得到serializer对象ser = CarouselModelSerializer(instance=queryset, many=True)print(ser.data)  # 测试成功删除# 手动添加地址前缀# serializer对象.data的值是一个有序字段, [{}, {}, ...}for dic in ser.data:  # dic['img'] ==> '/media/Carousel/Carouse1.png'dic['img'] = 'https://127.0.0.1:8000' + dic['img']# 将数据写入到redis中cache.set('Carousel_list', ser.data)# 获取数据  print(cache.get('Carousel_list'))  # 测试成功删除return True
* 11. 删除数据
删除一个拖, is_delete=True, 操作之后记得更新到数据库!

* 12. 启动beat端
celery beat -A celery_task -l info
添加一次任务后, ctrl + c 终止beat端

# 从数据库取出的数据
[OrderedDict([('name', 'Carouse2'), ('link', 'actual-course'), ('img', '/media/Carousel/Carouse2.png')]),
OrderedDict([('name', 'Carouse3'), ('link', 'light-course'), ('img', '/media/Carousel/Carouse3.png')])]# 从redis中取出的值数据(拼接地址写入后的结果)
[OrderedDict([('name', 'Carouse2'), ('link', 'actual-course'), ('img', 'https://127.0.0.1:8000/media/Carousel/Carouse2.png')]), OrderedDict([('name', 'Carouse3'), ('link', 'light-course'), ('img', 'https://127.0.0.1:8000/media/Carousel/Carouse3.png')])]

* 13. 删除测试代码, 将删除的照片恢复.print(ser.data)  # 测试成功删除print(ser.data)  # 测试成功删除

3. 免费课程群查接口

3.1 创建课程app

* 1. 创建course app
PS F:\synchro\Project\luffy> cd .\luffy\apps\
PS F:\synchro\Project\luffy\luffy\apps> python ../../manage.py startapp course
* 2. 在settings/dev.py中注册course  app
INSTALLED_APPS = [...'course',
]
* 3. 配置路由(主路由 urls.py)
urlpatterns = [...path('course/', include('course.urls'))
]
* 4. 在course 目录下新建urls.py文件(子路由)
from django.contrib import admin
from django.urls import pathurlpatterns = [path('admin/', admin.site.urls),
]

3.2 关系字段(补充)

ForeignKey参数: (设置要关联的表的字段)
related_name='xxx' : 反向操作时, 使用的字段名, 用与代替原反向查询时的'表名小写_set'
related_quert_name='xx' : 反向查询操作时, 使用'xx__', 代替 '虚拟字段名__'.
# 定义班级表
class Classes(models.Model):# 名字name = models.CharField(max_length=32)# 定义学生表
class Student(models.Model):# 名称name = models.CharField(max_length=32)# 外键(theclass 类)theclass = models.ForeignKey(to='Classes', related_name='students', related_quert_name='x')
要查询某个班级关联的所有学生(反向查询):
models.Classes.objects.first().student_set.all()
models.Classes.objects.values('theclass__name')
在ForeignKey中设置了related_name参数后:
models.Classes.objects.first().students.all()
在ForeignKey中设置了related_quert_name参数后:
models.Classes.objects.values('x__name')

3.3 表设置

1. 基础表
from django.db import models# Create your models here.
# 基础表(等会使用被人的数据, 自己不想该原来的基础表, 就在创建一个)
class BaseModel(models.Model):# 是否删除is_delete = models.BooleanField(default=False, verbose_name='是否删除')# 是否展示is_show = models.BooleanField(default=True, verbose_name='是否展示')# 创建时间created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')# 最后更新时间updated_time = models.DateTimeField(auto_now=True, verbose_name='最后更新时间')# 排序orders = models.IntegerField(verbose_name='排序')# 定义元类class Meta:abstract = True  # 不创建表
2. 分类表
# 课程分类表
class CourseCategory(BaseModel):"""课程分类表关联课程表, 分类表(一) 对 (多)课程表"""# 课程名称, 名称唯一name = models.CharField(max_length=64, unique=True, verbose_name='分类名称')# 定义元类class Meta:# 定义 数据库表名db_table = 'luffy_course_category'# 定义一个变量verbose_name = '分类'# 在后台展示的表名, 不带sverbose_name_plural = verbose_name# 打印对象时展示分类的名称def __str__(self):return f'{self.name}'
3. 导师表
# 导师表
class Teacher(BaseModel):"""导师表关联课程表, 导师表(一) 对 (多)课程表 """role_choices = ((0, '讲师'),(1, '导师'),(2, '班主任'))# 导师名称name = models.CharField(max_length=32, verbose_name='导师名称')# 角色 SmallIntegerField 小整型, 默认为0 教师身份role = models.SmallIntegerField(choices=role_choices, default=0, verbose_name='导师身份')# 职位title = models.CharField(max_length=64, verbose_name='职位|职称')# 签名signature = models.CharField(max_length=255, null=True, blank=True, verbose_name='导师签名', help_text='导师签名')# blank=True 在admin中新增数据,可以为空,默认是 False,这个只是前端的校验,null=True是后端的校验# 导师封面image = models.ImageField(upload_to='teach', null=True, blank=True, verbose_name='导师封面')# 导师描述brief = models.TextField(max_length=1024, verbose_name='导师描述')# 定义元类class Meta:# 定义 数据库表名db_table = 'luffy_teacher'# 定义一个变量verbose_name = '导师'# 在后台展示的表名, 不带sverbose_name_plural = verbose_name# 打印对象时展示导师的名称def __str__(self):return f'{self.name}'
4. 课程表
# 课程表
class Course(BaseModel):"""课程分类表(一) 对 (多) 课程表导师表(一) 对 (多) 课程表"""# 课程类型course_type = ((0, '付费'),(1, 'VIP专享'),(2, '学位课程'))# 级别选择level_choices = ((0, '初级'),(1, '中级'),(2, '高级'))# 状态选择status_choices = ((0, '上线'),(1, '下线'),(2, '预上线'))# 课程名称name = models.CharField(max_length=128, verbose_name='课程名称')# 封面图片course_img = models.ImageField(upload_to='course', max_length=255, null=True, blank=True, verbose_name='封面图片')# 付费类型course_type = models.SmallIntegerField(choices=course_type, default=0, verbose_name='付费类型')# 详情介绍brief = models.TextField(max_length=2048, null=True, blank=True, verbose_name='详情介绍')# 难度等级level = models.SmallIntegerField(choices=level_choices, default=0, verbose_name='难度等级')# 发布时间pub_date = models.DateTimeField(auto_now_add=True, verbose_name='发布时间')# 建议学习周期(day)period = models.IntegerField(default=7, verbose_name='建议学习周期(day)')# 课件路径attachment_path = models.FileField(upload_to='attachment', max_length=128,null=True,blank=True,verbose_name='课件路径')# 课程状态status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name='课程状态')# 课程原价price = models.DecimalField(max_digits=6, decimal_places=2, defaule=0, verbose_name='课程原价')# 优化字段# 学习人数students = models.IntegerField(default=0, verbose_name='学习人数')# 总课时数量sections = models.IntegerField(default=0, verbose_name='总课时数量')# 课时更新数量pub_sections = models.IntegerField(default=0, verbose_name='课时更新数量')# 外键# 课程分类, 删除外键是设置为空 db_constraint 数据库约束关闭course_category = models.ForeignKey(to='CourseCategory', on_delete=models.SET_NULL, null=True, blank=True,db_constraint=False,verbose_name='课程分类')# 导师表 DO_NOTHING 删除数据时什么都不做 db_constraint 数据库约束关闭teacher = models.ForeignKey(to='Teacher', on_delete=models.DO_NOTHING, null=True, blank=True, db_constraint=False,verbose_name='授课老师', )# 定义元类class Meta:# 定义 数据库表名db_table = 'luffy_course'# 定义一个变量verbose_name = '课程'# 在后台展示的表名, 不带sverbose_name_plural = verbose_name# 打印对象时展示课程的名称def __str__(self):return f'{self.name}'
5. 章节表
# 章节表
class CourseChapter(BaseModel):"""课程表(一) 对 (多)章节表"""# 章节名称chapter = models.SmallIntegerField(default=1, verbose_name="第几章")# 章节标题name = models.CharField(max_length=128, verbose_name="章节标题")# 章节介绍summary = models.TextField(null=True, blank=True, verbose_name="章节介绍")# 发布日期pub_date = models.DateField(auto_now_add=True, verbose_name="发布日期")# 外键# 课程名称course = models.ForeignKey(to="Course", related_name='course_chapters', on_delete=models.CASCADE,db_constraint=False, verbose_name="课程名称")# 定义元类class Meta:# 定义 数据库表名db_table = "luffy_course_chapter"# 定义一个变量verbose_name = "章节"# 在后台展示的表名, 不带sverbose_name_plural = verbose_name# 打印对象时展示 课程的名称 第几章节 章节名字def __str__(self):return f'{self.course}: 第{self.chapter}章{self.name}'
6. 课时表
# 课时表
class CourseSection(BaseModel):"""章节表 (一) 对 (多) 课时表"""# 部分类型选择section_type_choices = ((0, '文档'),(1, '练习'),(2, '视频'))# 课时标题name = models.CharField(max_length=128, verbose_name='课时标题')# 课时排序 PositiveIntegerField 正整数类型orders = models.PositiveIntegerField(verbose_name='课时排序')# 课时种类 SmallIntegerField 小整数类型section_type = models.SmallIntegerField(choices=section_type_choices, default=2, verbose_name='课时种类')# 课时链接section_link = models.CharField(max_length=255, null=True, blank=True, verbose_name='课时链接',help_text='若是video填vid, 若是文档, 填link')# 视频时长duration = models.CharField(max_length=32, null=True, blank=True, verbose_name='视频时长')# 发布时间pub_data = models.DateTimeField(auto_now_add=True, verbose_name='发布时间')# 是否可以试看free_trail = models.BooleanField(default=False, verbose_name='是否可以试看')# 外键chapter = models.ForeignKey(to='CourseChapter',related_name='course_sections',on_delete=models.CASCADE,db_constraint=False,verbose_name='课程章节')# 定义元类class Meta:# 定义 数据库表名db_table = 'luffy_course_Section'# 定义一个变量verbose_name = '课时'# 在后台展示的表名, 不带sverbose_name_plural = verbose_name# 打印对象时展示章节与课时的名称def __str__(self):return f'{self.chapter} - {self.name}'
7. 数据量迁移
python manage.py makemigrations
python manage.py migrate
8. 注册表到后台
在Course App下的admin.py 中将创建的表注册到后台.
import xadmin
from . import models# 分类表
xadmin.site.register(models.CourseCategory)
# 导师表
xadmin.site.register(models.Teacher)
# 课程表
xadmin.site.register(models.Course)
# 章节表
xadmin.site.register(models.CourseChapter)
# 课时表
xadmin.site.register(models.CourseSection)
9. 后台菜单改为中文
Django 实现xadmin后台菜单改为中文
* 1. 应用目录下apps.py
from django.apps import AppConfigclass CourseConfig(AppConfig):name = 'course'verbose_name = "课程表"  # 添加
* 2. 应用目录下__init__.py
default_app_config = "course.apps.CourseConfig"

3.4 写入数据

1. 图片资源
在luffy/media/下新建 teacher目录 与 course目录头像图片放在 media/teacher 文件夹下
课程图片放在 media/course 文件夹下
2. 分类表
使用navicat传数据.

INSERT INTO luffy_course_category(id, orders, is_show, is_delete, created_time, updated_time, name) VALUES (1, 1, 1, 0, '2019-07-14 13:40:58.690413', '2019-07-14 13:40:58.690477', 'Python');INSERT INTO luffy_course_category(id, orders, is_show, is_delete, created_time, updated_time, name) VALUES (2, 2, 1, 0, '2019-07-14 13:41:08.249735', '2019-07-14 13:41:08.249817', 'Linux');
3. 导师表
INSERT INTO luffy_teacher(id, orders, is_show, is_delete, created_time, updated_time, name, role, title, signature, image, brief) VALUES (1, 1, 1, 0, '2019-07-14 13:44:19.661327', '2019-07-14 13:46:54.246271', '无极剑圣·易', 1, '教学总监', '王牌', 'teacher/无极剑圣.png', '无极剑圣·易,是MOBA竞技网游《英雄联盟》中的一位英雄角色,英雄定位为战士、刺客。易大师是一个有超高机动性的刺客、战士型英雄,擅长利用快速的打击迅速击溃对手,易大师一般打野和走单人路,作为无极剑道的最后传人,易可以迅速砍出大量伤害,同时还能利...');INSERT INTO luffy_teacher(id, orders, is_show, is_delete, created_time, updated_time, name, role, title, signature, image, brief) VALUES (2, 2, 1, 0, '2019-07-14 13:45:25.092902', '2019-07-14 13:45:25.092936', '永恒梦魇', 0, '导师1', '金牌', 'teacher/永恒梦魇.png', '永恒梦魇是英雄联盟当中一个很强力的打野英雄,由于他的被动效果和Q技能导致他的清野效率非常的高。Q技能具有AOE伤害,而被动效果具有溅射伤害,可以快速帮助他刷完石头以及大鸟的...');INSERT INTO luffy_teacher(id, orders, is_show, is_delete, created_time, updated_time, name, role, title, signature, image, brief) VALUES (3, 3, 1, 0, '2019-07-14 13:46:21.997846', '2019-07-14 13:46:21.997880', '武器大师·贾克斯', 0, '导师2', '银牌', 'teacher/武器大师.png', '武器大师·贾克斯,是MOBA竞技网游《英雄联盟》中的英雄角色。贾克斯是一个万金油型的英雄,他可以适应任何阵容。取得优势的武器可以输出成吨的伤害,而且魔法和物理的混合伤害也让对面..');
4. 课程表
INSERT INTO luffy_course(id, orders, is_show, is_delete, created_time, updated_time, name, course_img, course_type, brief, level, pub_date, period, attachment_path, status, students, sections, pub_sections, price, course_category_id, teacher_id) VALUES (1, 1, 1, 0, '2019-07-14 13:54:33.095201', '2019-07-14 13:54:33.095238', 'Python开发21天入门', 'courses/python1.png', 0, 'Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土', 0, '2019-07-14', 21, '', 0, 231, 120, 120, 0.00, 1, 1);INSERT INTO luffy_course(id, orders, is_show, is_delete, created_time, updated_time, name, course_img, course_type, brief, level, pub_date, period, attachment_path, status, students, sections, pub_sections, price, course_category_id, teacher_id) VALUES (2, 2, 1, 0, '2019-07-14 13:56:05.051103', '2019-07-14 13:56:05.051142', 'Python项目实战', 'courses/python2.png', 0, 'python进阶', 1, '2019-07-14', 30, '', 0, 340, 120, 120, 99.00, 1, 2);INSERT INTO luffy_course(id, orders, is_show, is_delete, created_time, updated_time, name, course_img, course_type, brief, level, pub_date, period, attachment_path, status, students, sections, pub_sections, price, course_category_id, teacher_id) VALUES (3, 3, 1, 0, '2019-07-14 13:57:21.190053', '2019-07-14 13:57:21.190095', 'Linux系统基础5周入门精讲', 'courses/linux.png', 0, 'linux基础', 0, '2019-07-14', 25, '', 0, 219, 100, 100, 39.00, 2, 3);
5. 章节表
INSERT INTO luffy_course_chapter(id, orders, is_show, is_delete, created_time, updated_time, chapter, name, summary, pub_date, course_id) VALUES (1, 1, 1, 0, '2019-07-14 13:58:34.867005', '2019-07-14 14:00:58.276541', 1, '计算机原理', '', '2019-07-14', 1);INSERT INTO luffy_course_chapter(id, orders, is_show, is_delete, created_time, updated_time, chapter, name, summary, pub_date, course_id) VALUES (2, 2, 1, 0, '2019-07-14 13:58:48.051543', '2019-07-14 14:01:22.024206', 2, '环境搭建', '', '2019-07-14', 1);INSERT INTO luffy_course_chapter(id, orders, is_show, is_delete, created_time, updated_time, chapter, name, summary, pub_date, course_id) VALUES (3, 3, 1, 0, '2019-07-14 13:59:09.878183', '2019-07-14 14:01:40.048608', 1, '项目创建', '', '2019-07-14', 2);INSERT INTO luffy_course_chapter(id, orders, is_show, is_delete, created_time, updated_time, chapter, name, summary, pub_date, course_id) VALUES (4, 4, 1, 0, '2019-07-14 13:59:37.448626', '2019-07-14 14:01:58.709652', 1, 'Linux环境创建', '', '2019-07-14', 3);
6. 课时表
INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (1, 1, 0, '2019-07-14 14:02:33.779098', '2019-07-14 14:02:33.779135', '计算机原理上', 1, 2, NULL, NULL, '2019-07-14 14:02:33.779193', 1, 1);INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (2, 1, 0, '2019-07-14 14:02:56.657134', '2019-07-14 14:02:56.657173', '计算机原理下', 2, 2, NULL, NULL, '2019-07-14 14:02:56.657227', 1, 1);INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (3, 1, 0, '2019-07-14 14:03:20.493324', '2019-07-14 14:03:52.329394', '环境搭建上', 1, 2, NULL, NULL, '2019-07-14 14:03:20.493420', 0, 2);INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (4, 1, 0, '2019-07-14 14:03:36.472742', '2019-07-14 14:03:36.472779', '环境搭建下', 2, 2, NULL, NULL, '2019-07-14 14:03:36.472831', 0, 2);INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (5, 1, 0, '2019-07-14 14:04:19.338153', '2019-07-14 14:04:19.338192', 'web项目的创建', 1, 2, NULL, NULL, '2019-07-14 14:04:19.338252', 1, 3);INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (6, 1, 0, '2019-07-14 14:04:52.895855', '2019-07-14 14:04:52.895890', 'Linux的环境搭建', 1, 2, NULL, NULL, '2019-07-14 14:04:52.895942', 1, 4);

3.5 分类接口

访问分类表, 获取所有分类的id与名称.
* 后续点击该分类时, 提交分类的id, 通过进行赛选!
* 1. 路由层
from django.urls import re_path, includefrom . import views
# 导入SimpleRouter
from rest_framework.routers import SimpleRouter# 生成对象
router = SimpleRouter()
# 注册路由, 课程分类
router.register('categories', views.CourseCategoryView, 'categories')# 路由列表
urlpatterns = [re_path('', include(router.urls)),
]
* 2. 模型序列化器
# 导入模型序列化
from rest_framework import serializers# 导入模型成
from . import models# 分类表模型序列化器
class CourseCategoryModelsSerializer(serializers.ModelSerializer):class Meta:# 使用的表模型model = models.CourseCategory# 转换的字段fields = ['id', 'name']
* 3. 视图类
# 导入数图类
from rest_framework.viewsets import GenericViewSet
# 导入视图子类
from rest_framework.mixins import ListModelMixin# 导入模型层
from . import models# 导入序列化器
from . import serializer# 导入自定义异常模块
from utils import api_response# 分类接口 ViewSet
class CourseCategoryView(GenericViewSet, ListModelMixin):# 使用分类表模型queryset = models.CourseCategory.objects.filter(is_delete=False, is_show=True)# 使用序列化器serializer_class = serializer.CourseCategoryModelsSerializer# 自定义list方法def list(self, request, *args, **kwargs):res = super().list(self, request, *args, **kwargs)return api_response.ResponseDataFormat(data=res.data)
* 4. 测试地址: http://127.0.0.1:8000/course/categories/

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pcjc7Zvr-1654244938465)(C:/Users/13600/AppData/Roaming/Typora/typora-user-images/image-20220530233520830.png)]

3.6 课程接口测试

需要展示的字段:
课程id, 课程name, 课程封面图片, 课程详情, 导师, 课程类型名称, 课程状态, 难度等级
四个课时名称
* 1. 路由
# urls.py
# 免费课程接口
router.register('free', views.CourseView, 'free')
* 2. 序列化类 (子序列化方法, 字段是另一个模型表的数据)
# apps/course/serializer.py
# 导师表模型序列化器
class TeacherModelsSerializer(serializers.ModelSerializer):class Meta:# 使用的表模型model = models.Teacher# 转换的字段, 导师的名字, 职位, 角色名称fields = ['name', 'title', 'role_name']# 课程表模型序列化器
class CourseModelSerializer(serializers.ModelSerializer):# 子序列化方法获取导师表的数据, 使用了子序列虎, 生成对象, 这个对应一定要在fields中注册, 否则会报错teacher = TeacherModelsSerializer()class Meta:# 使用的表模型model = models.Course# 转换的字段fields = [# 课程id'id',# 课程名称'name',# 封面'course_img',# 导师的名字'teacher',# 详情介绍'brief',# 课程类型名称'course_type_name',# 难度等级名称'level_name',# 状态名称'status_name',# 获取课时(四个章节)'get_course_chapter']
序列化类中使用的字段可以是模型类的中方法.
模型类中的方法使用@property 将方法装饰为属性.
# apps/course/models.py 下的Teacher表模型下添加role_name方法# 导师角色对应的值
@property  # 将方法伪装为属性, 方法名不要与字段重复!, 否则会有冲突
def role_name(self):return self.get_role_display()# -------------------------------------------------------------------# apps/course/models.py 下的Course表模型下添加course_type_name / level_name / status_name
# 获取课程类型名称
@property
def course_type_name(self):return self.get_course_type_display()# 等级名称
@property
def level_name(self):return self.get_level_display()# 状态名称
@property
def status_name(self):return self.get_status_display()# 查看课程章节@propertydef get_course_chapter(self):"""1. 先获取到所有章节表的对象课程表查章节表 --> 反向查询 --> self.表名小写_setself是课程表对象, 章节表中外键绑定课程表设置了 related_name='course_chapters'course_chapters 替代 表名小写_set"""all_chapter_obj = self.course_chapters.all()"""2. 遍历章节对象, 章节对象反向查询课时表所有对象, 再遍历课时表获取课时名称(四个即可)课时表表中外键绑章节表设置了 related_name='course_sections',"""# 定义一个空列表chapter_list = []for chapter_obj in all_chapter_obj:for course_section_obj in chapter_obj.course_sections.all():# 组织格式chapter_list.append({# 课时的名称'name': course_section_obj.name,# 课时的链接'link': course_section_obj.section_link})# 获取到四个课时则返回if len(chapter_list) == 4:return chapter_list# 不够四个课时有几个拿几个return chapter_list
* 3. 分页器
# apps/course/pagination
# 导入内置的页码分页器类
from rest_framework.pagination import PageNumberPagination as DRFPageNumberPagination# 继承页码分页器类定义分页的数据属性值
class PageNumberPagination(DRFPageNumberPagination):# 每页的数目 一条数据page_size = 1# 页数关键字名, 默认为 pagepage_query_param = 'page'# 每页数目关键字, 默认为Nonepage_size_query_param = 'page_size'# 每页最大的显示条数max_page_size = 10
* 4. 视图类
# 课程接口
class CourseView(GenericViewSet, ListModelMixin):# 使用课程表模型queryset = models.Course.objects.filter(is_delete=False, is_show=True)# 使用的序列化器serializer_class = serializer.CourseModelSerializer# 导入分页器from .paginations import PageNumberPagination# 分页器pagination_class = PageNumberPagination# 自定义list方法def list(self, request, *args, **kwargs):res = super().list(self, request, *args, **kwargs)return api_response.ResponseDataFormat(data=res.data)
* 5. 测试地址 http://127.0.0.1:8000/course/free/

3.7 排序过滤测试

使用rest_framework内置的排序模块(指定的字段不能是外键!)
* 1. 修改 apps/course/serializer.py 文件, 测试排序, 不展示太多的字段信息
# 课程表模型序列化器
class CourseModelSerializer(serializers.ModelSerializer):class Meta:# 使用的表模型model = models.Course# 转换的字段fields = [# 课程id'id',# 课程名称'name',# 价格'price',]
* 2. 修改视图类, 测试排序不需要使用分页器.
# 修改
# 课程接口
class CourseView(GenericViewSet, ListModelMixin):# 使用课程表模型queryset = models.Course.objects.filter(is_delete=False, is_show=True)# 使用的序列化器serializer_class = serializer.CourseModelSerializer# 排序 / 过滤from rest_framework.filters import OrderingFilter, SearchFilter# 配置过滤和排序(把优先级高的刚在前面)filter_backends = [OrderingFilter, SearchFilter]# 排序的字段(可以同时指定多个字段排序, 先按第一个字段排序, 在按第二个字段排序)ordering_fields = ['id', 'price']# 过滤的字段search_fields = ['id']# 自定义list方法def list(self, request, *args, **kwargs):res = super().list(self, request, *args, **kwargs)return api_response.ResponseDataFormat(data=res.data)
1. 排序源码分析
1. 视图类继承了GenericViewSet与ListModelMixinGenericViewSet 继承了 (ViewSetMixin, generics.GenericAPIView)
2. 在路由匹配成功之后会执行list方法.
# list方法
class ListModelMixin:def list(self, request, *args, **kwargs):# 过滤查询(过滤走这一步)queryset = self.filter_queryset(self.get_queryset())# 分页page = self.paginate_queryset(queryset)if page is not None:serializer = self.get_serializer(page, many=True)return self.get_paginated_response(serializer.data)# 序列化serializer = self.get_serializer(queryset, many=True)# 返回序列化结果return Response(serializer.data)
3. queryset = self.filter_queryset(self.get_queryset())self是当前视图类, self.get_queryset是GenericAPIView提供的, 获取数据对象,filter_queryset, 也是GenericAPIView提供的.

4. filter_queryset方法中使用视图类的filter_backends属性.
filter_backends属性在单独配置的时候值可以是单独的一个对象OrderingFilter视图类中
filter_backends = [OrderingFilter]   list([OrderingFilter])还是[OrderingFilter] 数图类中使用 filter_backends = [排序类, 过滤类] 遍历这个列表生成一个个的对象.如果视图类中
filter_backends = OrderingFilter 则 list(OrderingFilter) => [OrderingFilter]
# 过滤方法
def filter_queryset(self, queryset):# 从数图类中获取过滤模类for backend in list(self.filter_backends):# backend()生成对象, 执行OrderingFilter对象的filter_queryset方法# 如果是过滤对象则执行过滤对象的filter_queryset方法, (鸭子类型嘎嘎)# queryset是过滤&&排序之后的queryset对象queryset = backend().filter_queryset(self.request, queryset, self)return queryset
5. OrderingFilter对象的filter_queryset方法
def filter_queryset(self, request, queryset, view):# 调用OrderingFilter对象执行get_ordering方法 得到一个列表, 列表中有合格的排序字段ordering = self.get_ordering(request, queryset, view)# 排序列表有值if ordering:return queryset.order_by(*ordering)return queryset
6. OrderingFilter对象的get_ordering方法
def get_ordering(self, request, queryset, view):"""   获取路由地址?ordering=xx的值,  self.ordering_paramordering_param = api_settings.ORDERING_PARAM 从配置文件中获取 'ORDERING_PARAM': 'ordering'支持 ordering=id, xx (多值) 结果是一个字符串 'id, xx'"""params = request.query_params.get(self.ordering_param)# 如果有值修改值的格式if params:"""params.split(',') 字符串按,切分得到一个列表['id', 'xx']从列表中取出一个个值, ''.strip() 取出移除字符串头尾指定的字符(默认为空格)"""fields = [param.strip() for param in params.split(',')]# 去除无效过滤条件ordering = self.remove_invalid_fields(queryset, fields, view, request)# ordering是一个['id', 'xx'] 合格的排序字段if ordering:return orderingreturn self.get_default_ordering(view)
* 7. 排序测试按价格排序(正序): http://127.0.0.1:8000/course/free/?ordering=price按价格排序(倒序): http://127.0.0.1:8000/course/free/?ordering=-price

2. 过滤源码分析
1. 执行过滤对象的filter_queryset方法
# GenericAPIView的过滤方法
def filter_queryset(self, queryset):# 从视图类中获取过滤模类for backend in list(self.filter_backends):# backend()生成对象, 执行OrderingFilter对象的filter_queryset方法# 如果是过滤对象则执行过滤对象的filter_queryset方法, (鸭子类型嘎嘎)# queryset是过滤&&排序之后的queryset对象queryset = backend().filter_queryset(self.request, queryset, self)return queryset
* 2. SearchFilter过滤对象filter_queryset的方法源码
# SearchFilter的 get_search_fields 方法
def get_search_fields(self, view, request):# 获取视图类的search_fields属性值return getattr(view, 'search_fields', None)
# SearchFilter的 get_search_terms 方法
def get_search_terms(self, request):"""self.search_param search_param => api_settings.SEARCH_PARAM => 'SEARCH_PARAM': 'search',获取?search=''的值, 值是一个字段串, 值可以有多个使用, 空格分隔 """ params = request.query_params.get(self.search_param, '')# 去除空字段, 将'\x00'替换为 ''params = params.replace('\x00', '')  # strip null characters# 在将, 替换为 ' 'params = params.replace(',', ' ')# 在按' '切分得到一个列表return params.split()
# SearchFilter的 lookup_prefixes属性 与 construct_search 方法
# 查找前缀
lookup_prefixes = {'^': 'istartswith','=': 'iexact','@': 'search','$': 'iregex',
}# 构造查询条件的名字
def construct_search(self, field_name):# 这里的field_name则是'id',  'id'[0] => 'i' => lookup_prefixes.get('i') => None# 第一的字符是 ^ = @ $ 则 field_name 等于 istartswith iexact search iregex lookup = self.lookup_prefixes.get(field_name[0])# 如果值是^xx =xx @xx $xx 后面跟着的值, 拼接成xx_istartswith xx_iexact ...if lookup:field_name = field_name[1:]else:lookup = 'icontains'# 如果lookup没有值 拼接成 过滤字段名_icontains return LOOKUP_SEP.join([field_name, lookup])
# SearchFilter的 filter_queryset 方法def filter_queryset(self, request, queryset, view):# 通过反射获取视图类中设置的过滤的字段search_fields = self.get_search_fields(view, request)# 前端传递的过滤值search_terms = self.get_search_terms(request)# 过滤的值为空, 就直接放回不过滤了if not search_fields or not search_terms:return queryset# 拼接过滤字段的值orm_lookups = [self.construct_search(str(search_field))for search_field in search_fields]# ↓ 遍历过滤的值和过滤的字段去赛选... base = querysetconditions = []for search_term in search_terms:queries = [models.Q(**{orm_lookup: search_term})for orm_lookup in orm_lookups]conditions.append(reduce(operator.or_, queries))queryset = queryset.filter(reduce(operator.and_, conditions))if self.must_call_distinct(queryset, search_fields):queryset = distinct(queryset, base)# 返回过滤的结果return queryset
* 3. 测试地址 http://127.0.0.1:8000/course/free/?search=2过滤条件id=2, search==id

3. django-filter过滤方式1
rest_framework 的SearchFilter类只支持过滤模型表基本字段, 不支持外键字段.
django-filter模块支持以外键字段过滤.# 导入django-filters
from django_filters.rest_framework import DjangoFilterBackend# 视图类中指定django-filters的过滤字段
filter_fields = ['course_category']# 使用: 赛选字段使用字段名称  ?course_category=值
* 1. 安装django-filter模块命令: pip install django-filter
* 2. 使用django-filter模块
# 课程接口
class CourseView(GenericViewSet, ListModelMixin):# 使用课程表模型queryset = models.Course.objects.filter(is_delete=False, is_show=True)# 使用的序列化器serializer_class = serializer.CourseModelSerializer# 导入django-filtersfrom django_filters.rest_framework import DjangoFilterBackend# 配置过滤和排序filter_backends = [DjangoFilterBackend]# 指定django-filters的过滤字段filter_fields = ['course_category']# 自定义list方法def list(self, request, *args, **kwargs):res = super().list(self, request, *args, **kwargs)return api_response.ResponseDataFormat(data=res.data)
* 3. 测试地址: http://127.0.0.1:8000/course/free/?course_category=1course_category=1, course_category外键1对应这课程为Python的课程

4. django-filter过滤方式2
* 1. 在apps/scourse下新建filter_test.py 文件
# 导入FilterSet模块
from django_filters.filterset import FilterSet
from . import modelsclass CourseFilterSet(FilterSet):class Meta:model = models.Coursefields = ['course_category']
* 2. 视图类中使用
# 课程接口
class CourseView(GenericViewSet, ListModelMixin):# 使用课程表模型queryset = models.Course.objects.filter(is_delete=False, is_show=True)# 使用的序列化器serializer_class = serializer.CourseModelSerializer# 导入django_filter模块from django_filters.rest_framework import DjangoFilterBackend# 配置过滤filter_backends = [DjangoFilterBackend]# 导入自定义的过滤类from .filter_test import CourseFilterSet# 配置类, 自定义的类filter_class = CourseFilterSet# 自定义list方法def list(self, request, *args, **kwargs):res = super().list(self, request, *args, **kwargs)return api_response.ResponseDataFormat(data=res.data)
* 3. 测试地址: http://127.0.0.1:8000/course/free/?course_category=1

5.django-filter区间过滤
* 1. 修改CourseFilterSet类
# 导入FilterSet模块
from django_filters.filterset import FilterSet
from . import models
from django_filters import filtersclass CourseFilterSet(FilterSet):# 课程的价格范围 field_name 指定字段 gt大于min_price,lt小于max_price    min_price = filters.NumberFilter(field_name='price', lookup_expr='gt')max_price = filters.NumberFilter(field_name='price', lookup_expr='lt')class Meta:model = models.Coursefields = ['course_category']
* 2. 测试地址: http://127.0.0.1:8000/course/free/?max_price=100&min_price=-1min_price=0 的意思是 大于0的意思 如果 ?max_price=100&min_price=-1 则id为1的数据不会显示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IUMQFnRF-1654244938469)(C:/Users/13600/AppData/Roaming/Typora/typora-user-images/image-20220601232545399.png)]

* 3. 如果提示没有模块, 则在配置文件中注册django_filters

INSTALLED_APPS = [...'django_filters',
]
6.django-filter源码分析
# 导入django-filters过滤器
from django_filters.rest_framework import DjangoFilterBackend# 配置过滤和排序配置
filter_backends = [DjangoFilterBackend]
# 方式1 指定django-filters的过滤字段
filter_fields = ['course']
# 方式2 指定django-filters的过滤属性是一个类
filter_class = CourseFilterSet
从执行 GenericAPIView的filter_queryset方法开始
# GenericAPIView的过滤方法
def filter_queryset(self, queryset):# 从视图类中获取过滤模类for backend in list(self.filter_backends):# backend()生成对象, 执行DjangoFilterBackend对象的filter_queryset方法# queryset是过滤&&排序之后的queryset对象queryset = backend().filter_queryset(self.request, queryset, self)return queryset
# DjangoFilterBackend的filter_queryset方法
def filter_queryset(self, request, queryset, view):# 将请求对象, 模型表对象, 视图对象传递给get_filterset方法, filterset = self.get_filterset(request, queryset, view)# 结果要么是None 要么是一个过滤类的对象if filterset is None:return queryset# 校验字段, 字段不符合直接抛出异常if not filterset.is_valid() and self.raise_exception:raise utils.translate_validation(filterset.errors)# 执行return filterset.qs
class BaseFilterSet:...@propertydef qs(self):if not hasattr(self, '_qs'):# 获取所有数据对象qs = self.queryset.all()  # self是过滤对象, 过滤对象设置了data, queryset, request属性# self.is_bound = data is not None, data是过滤条件参数if self.is_bound:# 过滤前验证表单self.errors# 执行过滤, BaseFilterSet的filter_queryset方法(不往后看源码了)qs = self.filter_queryset(qs)# 如果没有过滤条件则将所有数据对象返回, 否则返回过滤的结果self._qs = qsreturn self._qs
# DjangoFilterBackend的get_filterset方法
def get_filterset(self, request, queryset, view):# 将视图类对象与模型对象传给get_filterset_class方法filterset_class = self.get_filterset_class(view, queryset)# 返回的结果要么是一个过滤类, 要么是Noneif filterset_class is None:return None# kwargs = {'data': request.query_params, 'queryset': queryset, 'request': request,}kwargs = self.get_filterset_kwargs(request, queryset, view)# 返回过滤类对象return filterset_class(**kwargs)
# DjangoFilterBackend的get_filterset_class方法
def get_filterset_kwargs(self, request, queryset, view):return {'data': request.query_params,'queryset': queryset,'request': request,}
# DjangoFilterBackend的get_filterset_class方法
def get_filterset_class(self, view, queryset=None):# 获取视图中的filterset_class属性值, 值是一个类的地址filterset_class = getattr(view, 'filterset_class', None)# 获取视图中的filterset_fields属性值, 值是一个列表filterset_fields = getattr(view, 'filterset_fields', None)# 版本问题, 视图类中没有设置filterset_class属性 and 设置了filter_class属性 # TODO: remove assertion in 2.1if filterset_class is None and hasattr(view, 'filter_class'):# 提示filter_class因该设置为filterset_class, filter_class属性弃用# view.__class__.__name__ 获取数图类的名称utils.deprecate("`%s.filter_class` attribute should be renamed `filterset_class`."% view.__class__.__name__)# 获取filter_class的属性值给filterset_class属性filterset_class = getattr(view, 'filter_class', None)# 版本问题, 视图类中没有设置filterset_fields属性 and 设置了filter_fields属性# TODO: remove assertion in 2.1if filterset_fields is None and hasattr(view, 'filter_fields'):# filter_fields因该设置为filterset_fields, filter_fields属性弃用# view.__class__.__name__ 获取数图类的名称utils.deprecate("`%s.filter_fields` attribute should be renamed `filterset_fields`."% view.__class__.__name__)# 获取filter_fields属性的值给filterset_fields属性filterset_fields = getattr(view, 'filter_fields', None)# 如果filterset_class有值, 则说明视图类中使用的类if filterset_class:# 获取视图类filterset_class对应的类使用表模型filterset_model = filterset_class._meta.model# print(filterset_class._meta.model)  # <class 'course.models.Course'># print(filterset_class._meta.fields)  # ['course_category']# 如果 设置了模型类 and 表模型数据不为空# FilterSets do not need to specify a Meta classif filterset_model and queryset is not None:# 如果 表模型数据 不是 指定类中meta的model属性对应的表模型的子类, 则抛出异常, 模型不匹配# queryset.model, filterset_model的值是一样的# <class 'course.models.Course'> <class 'course.models.Course'># print(queryset.model, filterset_model)# issubclass(类型1, 类型2) 两个类型一样则不会抛出异常assert issubclass(queryset.model, filterset_model), \'FilterSet model %s does not match queryset model %s' % \(filterset_model, queryset.model)# 返回 过滤类return filterset_class# 获取视图中的filterset_fields属性值 不为空 and 模型表数据对象不为空if filterset_fields and queryset is not None:# 从父类中获取Meta属性, 默认是没有设置这个值的MetaBase = getattr(self.filterset_base, 'Meta', object)# 创建一个过滤类继承filterset.FilterSetclass AutoFilterSet(self.filterset_base):# 定义元类生(MetaBase的值这里默认为object)class Meta(MetaBase):# queryset.model 获取数据对象的模型表model = queryset.model# 过滤的字段为视图类中设置filterset_fields对应的列表fields = filterset_fields# 返回过滤类return AutoFilterSet# 没有设置 filterset_fields 没有设置filterset_class 返回Nonereturn None
class DjangoFilterBackend(metaclass=RenameAttributes):filterset_base = filterset.FilterSet...
# DjangoFilterBackend  继承  FilterSet
class FilterSet(filterset.FilterSet):...# FilterSet 继承filterset.FilterSet
class FilterSet(BaseFilterSet, metaclass=FilterSetMetaclass):pass# filterset.FilterSet继承 BaseFilterSet
class BaseFilterSet:...
7. 总结
排序:类配置属性: filter_backends=[OrderingFilter]配置排序的字段: ordering_fields=['排序的字段1', '排序的字段2']排序的使用: (按id 正序 倒序) http://127.0.0.1:8000/course/free/?ordering=排序的字段过滤:类配置属性: filter_backends=[SearchFilter]配置过滤的字段: search_fields = ['过滤的字段1', '过滤的字段2']过滤的使用:http://127.0.0.1:8000/course/free/?search=过滤的字段django-filter: (过滤方式1)类配置属性: filter_backends=[DjangoFilterBackend]配置过滤的字段: filter_fields = ['过滤的字段1', '过滤的字段2']过滤的使用:http://127.0.0.1:8000/course/free/?过滤的字段=过滤的值django-filter: (过滤方式2)类配置属性: filter_backends=[DjangoFilterBackend]配置过滤类: filter_class = CourseFilterSet过滤的使用:http://127.0.0.1:8000/course/free/?过滤的字段=过滤的值django-filter: (区间查询)类配置属性: filter_backends=[DjangoFilterBackend]配置过滤类: filter_class = CourseFilterSet定义过滤类:...min_price = filters.NumberFilter(field_name='price', lookup_expr='gt')max_price = filters.NumberFilter(field_name='price', lookup_expr='lt')区间的使用:http://127.0.0.1:8000/course/free/?max=xx&&min=xxx
8. 自定义过滤规则
OrderingFilter 与 SearchFilter都继承了BaseFilterBackend类.
自定义过滤规则类:
1. 需要继承BaseFilterBackend类.
2. 重写filter_queryset方法返回response对象.
* 1. 在apps/scourse/filter_test.py 文件下写自定义过滤规则获取课程价格大于60块的课程.
# 导入基础过滤类
from rest_framework.filters import BaseFilterBackend# 自定义过滤类
class MyFilter(BaseFilterBackend):def filter_queryset(self, request, queryset, view):# xx = request.query_params.get('xx') 获取值price = request.query_params.get('price')# 获取课程价格大于xx元的课程queryset_obj = queryset.filter(price__gt=price)return queryset_obj
* 2. 使用自定义过滤类
# 课程接口
class CourseView(GenericViewSet, ListModelMixin):# 使用课程表模型queryset = models.Course.objects.filter(is_delete=False, is_show=True)# 使用的序列化器serializer_class = serializer.CourseModelSerializer# 导入自定义过滤类from .filter_test import MyFilter# 配置过滤filter_backends = [MyFilter]# 过滤的字段search_fields = ['price']# 自定义list方法def list(self, request, *args, **kwargs):res = super().list(self, request, *args, **kwargs)return api_response.ResponseDataFormat(data=res.data)
* 3. 测试地址: http://127.0.0.1:8000/course/free/?price=60

3.8 课程&排序&过滤接口

需要展示的字段:
id (课程id) , name (课程名称), course_img (课程封面图片),
brief (课程详情), attachment_path (课件路径), pub_sections (课时更新数量)
price (课程价格), students (学习热度), period (建议学习周期), sections (总课时的数量)
course_type_name (课程类型名称), level_name (等级名称), status_name(状态名称)
teacher 导师表信息( name 导师名字, role_name 角色的值, title 职位, signature 导师签名,
image 导师封面, brief 导师描述),
section_list (name 课时标题, section_link 课时链接, duration 视频时长, free_trail 是否可以试看) 排序的字段: id (课程id), price (课程价格), students (学习热度)
过滤的字段: course_category
*  1. course app的路由
from django.urls import re_path, includefrom . import views
# 导入SimpleRouter
from rest_framework.routers import SimpleRouter# 生成对象
router = SimpleRouter()
# 注册路由, 课程分类
router.register('categories', views.CourseCategoryView, 'categories')# 路由列表
urlpatterns = [re_path('', include(router.urls)),
]
* 2. 模型层导师表与课程表添加了几个获取数据的方法.
# 导师表
class Teacher(BaseModel):"""导师表关联课程表, 导师表(一) 对 (多)课程表"""role_choices = ((0, '讲师'),(1, '导师'),(2, '班主任'))# 导师名称name = models.CharField(max_length=32, verbose_name='导师名称')# 角色 SmallIntegerField 小整型, 默认为0 教师身份role = models.SmallIntegerField(choices=role_choices, default=0, verbose_name='导师身份')# 职位title = models.CharField(max_length=64, verbose_name='职位|职称')# 签名signature = models.CharField(max_length=255, null=True, blank=True, verbose_name='导师签名', help_text='导师签名')# blank=True 在admin中新增数据,可以为空,默认是 False,这个只是前端的校验,null=True是后端的校验# 导师封面image = models.ImageField(upload_to='teach', null=True, blank=True, verbose_name='导师封面')# 导师描述brief = models.TextField(max_length=1024, verbose_name='导师描述')# 定义元类class Meta:# 定义 数据库表名db_table = 'luffy_teacher'# 定义一个变量verbose_name = '导师'# 在后台展示的表名, 不带sverbose_name_plural = verbose_name# 打印对象时展示导师的名称def __str__(self):return f'{self.name}'# 导师角色对应的值@property  # 将方法伪装为属性, 方法名不要与字段重复!, 否则会有冲突def role_name(self):return self.get_role_display()# 课程表
class Course(BaseModel):"""课程分类表(一) 对 (多) 课程表导师表(一) 对 (多) 课程表"""# 课程类型course_type = ((0, '付费'),(1, 'VIP专享'),(2, '学位课程'))# 级别选择level_choices = ((0, '初级'),(1, '中级'),(2, '高级'))# 状态选择status_choices = ((0, '上线'),(1, '下线'),(2, '预上线'))# 课程名称name = models.CharField(max_length=128, verbose_name='课程名称')# 封面图片course_img = models.ImageField(upload_to='course', max_length=255, null=True, blank=True, verbose_name='封面图片')# 付费类型course_type = models.SmallIntegerField(choices=course_type, default=0, verbose_name='付费类型')# 详情介绍brief = models.TextField(max_length=2048, null=True, blank=True, verbose_name='详情介绍')# 难度等级level = models.SmallIntegerField(choices=level_choices, default=0, verbose_name='难度等级')# 发布时间pub_date = models.DateTimeField(auto_now_add=True, verbose_name='发布时间')# 建议学习周期(day)period = models.IntegerField(default=7, verbose_name='建议学习周期(day)')# 课件路径attachment_path = models.FileField(upload_to='attachment', max_length=128,null=True,blank=True,verbose_name='课件路径')# 课程状态status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name='课程状态')# 课程原价price = models.DecimalField(max_digits=6, decimal_places=2, default=0, verbose_name='课程原价')# 优化字段# 学习人数students = models.IntegerField(default=0, verbose_name='学习人数')# 总课时数量sections = models.IntegerField(default=0, verbose_name='总课时数量')# 课时更新数量pub_sections = models.IntegerField(default=0, verbose_name='课时更新数量')# 外键# 课程分类, 删除外键是设置为空 db_constraint 数据库约束关闭course_category = models.ForeignKey(to='CourseCategory', on_delete=models.SET_NULL, null=True, blank=True,db_constraint=False,verbose_name='课程分类')# 导师表 DO_NOTHING 删除数据时什么都不做 db_constraint 数据库约束关闭teacher = models.ForeignKey(to='Teacher', on_delete=models.DO_NOTHING, null=True, blank=True, db_constraint=False,verbose_name='授课老师', )# 定义元类class Meta:# 定义 数据库表名db_table = 'luffy_course'# 定义一个变量verbose_name = '课程'# 在后台展示的表名, 不带sverbose_name_plural = verbose_name# 打印对象时展示课程的名称def __str__(self):return f'{self.name}'# 获取课程类型名称@propertydef course_type_name(self):return self.get_course_type_display()# 等级名称@propertydef level_name(self):return self.get_level_display()# 状态名称@propertydef status_name(self):return self.get_status_display()# 查看课程章节@propertydef section_list(self):"""1. 先获取到所有章节表的对象课程表查章节表 --> 反向查询 --> self.表名小写_setself是课程表对象, 章节表中外键绑定课程表设置了 related_name='course_chapters'course_chapters 替代 表名小写_set"""all_chapter_obj = self.course_chapters.all()"""2. 遍历章节对象, 章节对象反向查询课时表所有对象, 再遍历课时表获取课时名称(四个即可)课时表表中外键绑章节表设置了 related_name='course_sections',"""# 定义一个空列表chapter_list = []for chapter_obj in all_chapter_obj:for course_section_obj in chapter_obj.course_sections.all():# 组织格式chapter_list.append({# 课时的名称'name': course_section_obj.name,# 课时的链接'section_link': course_section_obj.section_link,# 视频时长'duration': course_section_obj.duration,# 是否可以试看'free_trail': course_section_obj.free_trail,})# 获取到四个课时则返回if len(chapter_list) == 4:return chapter_list# 不够四个课时有几个拿几个return chapter_list
* 3. 模型序列化器
# 导师表模型序列化器
class TeacherModelsSerializer(serializers.ModelSerializer):class Meta:# 使用的表模型model = models.Teacher# 转换的字段, 导师的名字, 职位, 角色名称, 导师签名, 导师封面, 导师描述fields = ['name', 'title', 'role_name', 'signature', 'image', 'brief']# 课程表模型序列化器
class CourseModelSerializer(serializers.ModelSerializer):# 子序列化方法获取导师表的数据teacher = TeacherModelsSerializer()class Meta:# 使用的表模型model = models.Course# 转换的字段fields = [# 课程id'id',# 课程名称'name',# 封面'course_img',# 课程详情'brief',# 课件路径'attachment_path',# 课时更新数量'pub_sections',# 课程价格'price',# 学习热度'students',# 建议学习周期'period',# 总课时的数量'sections',# 课程类型名称'course_type_name',# 等级名称'level_name',# 状态名称'status_name',# 导师表展示的字段'teacher',# 课时'section_list',]
* 4. 视图类
# 课程接口
class CourseView(GenericViewSet, ListModelMixin):# 使用课程表模型queryset = models.Course.objects.filter(is_delete=False, is_show=True)# 使用的序列化器serializer_class = serializer.CourseModelSerializer# 导入分页器from .paginations import PageNumberPaginationpagination_class = PageNumberPagination# 导入内置排序陌路from rest_framework.filters import OrderingFilter# 导入django-filters过滤器from django_filters.rest_framework import DjangoFilterBackend# 配置过滤和排序配置filter_backends = [OrderingFilter, DjangoFilterBackend]# 排序的字段(可以同时指定多个字段排序, 先按第一个字段排序, 在按第二个字段排序)ordering_fields = ['id', 'price', 'students']# 视图类中指定django-filters的过滤字段filter_fields = ['course_category']# 自定义list方法def list(self, request, *args, **kwargs):res = super().list(self, request, *args, **kwargs)return api_response.ResponseDataFormat(data=res.data)
* 5. 测试地址: http://127.0.0.1:8000/course/free/http://127.0.0.1:8000/course/free/?course_category=1 过滤测试(筛选python的课程)http://127.0.0.1:8000/course/free/?course_category=1&ordering=-id 筛选python的课程反向展示

4. 免费课程前端

如果后台不分页, 数据在response.data中. 如果后台分页, 数据在response.data.results中.
从data的count是数据总数量.
重写了list方法, 使用自定义的response返回数据.
获取数据:
this.course_list = response.data.data.results;
获取数据数量:
this.course_total = response.data.data.count;
<template><div class="course"><!-- 头部组件 --><Header></Header><!-- 课程展示页面 --><div class="main"><!-- 筛选条件 --><div class="condition"><!-- 按课程分类赛选 --><ul class="cate-list"><li class="title">课程分类:</li><!--点击全部的时候, 将filter.course_category的值设置为0filter.course_category==0 展示this样式--><li :class="filter.course_category==0?'this':''" @click="filter.course_category=0">全部</li><!--点击当前标签的时候, 将filter.course_category的值设置为类别对应的idfilter.course_category==当前类别的id 展示this样式通过判断 filter.course_category的值切换this全部    filter.course_category==0Python filter.course_category==1Linux  filter.course_category==2--><li :class="filter.course_category==category.id?'this':''" v-for="category in category_list"@click="filter.course_category=category.id" :key="category.name">{{ category.name }}</li></ul><!-- 按字段排序 --><div class="ordering"><ul><li class="title">筛&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;选:</li><!--点击默认的时候将ordering设置为'-id' 默认一值是倒序,filter.ordering的值是'id' 或 '-id' 都展示 this样式--><li class="default" :class="(filter.ordering=='id' || filter.ordering=='-id')?'this':''"@click="filter.ordering='-id'">默认</li><!--filter.ordering的值是'students' 或 '-students' 都展示 this样式点击人气的时候,第一次点击为将ordering 判断ordering的值 == '-students' 肯定不成立  ordering的值设置为-students第二次点击为将ordering 判断ordering的值 == '-students' 成立  ordering的值设置为 students--><li class="hot" :class="(filter.ordering=='students' || filter.ordering=='-students')?'this':''"@click="filter.ordering=(filter.ordering=='-students'?'students':'-students')">人气</li><!--点击价格将值设置为 -price, 在次点击设置为price当 ordering=='price' 时将 展示 price_up this样式当 ordering=='-price' 时将 展示 先执行 (filter.ordering=='-price'?'price_down this':'')再次判断ordering的值是否为'-price', 肯定是, 展示 price_down this样式--><li class="price":class="filter.ordering=='price'?'price_up this':(filter.ordering=='-price'?'price_down this':'')"@click="filter.ordering=(filter.ordering=='-price'?'price':'-price')">价格</li></ul><!-- 展示课程数量 --><p class="condition-result">共{{ course_total }}个课程</p></div></div><!-- 课程列表 --><div class="course-list"><!-- 遍历课程列表 --><div class="course-item" v-for="course in course_list" :key="course.name"><div class="course-image"><!--  课程封面  --><img :src="course.course_img" alt=""></div><div class="course-info"><h3><!--  课程名称 点击课程跳转到课程详情 --><router-link :to="'/free/detail/'+course.id">{{ course.name }}</router-link><!--  学习人数  --><span><img src="@/assets/img/avatar1.svg" alt="">{{ course.students }}人已加入学习</span></h3><p class="teacher-info"><!-- 导师姓名 职位 签名 -->{{ course.teacher.name }} {{ course.teacher.title }} {{ course.teacher.signature }}<!-- 课时更新展示 --><span v-if="course.sections>course.pub_sections">共{{ course.sections }}课时/已更新{{course.pub_sections}}课时</span><span v-else>共{{ course.sections }}课时/更新完成</span></p><!--  课时展示  --><ul class="section-list"><!-- key从o开始 从1开始则 key+1  --><li v-for="(section, key) in course.section_list" :key="section.name"><span class="section-title">0{{ key + 1 }}  |  {{ section.name }}</span><!--如果课程免费, 展示免费字样--><span class="free" v-if="section.free_trail">免费</span></li></ul><div class="pay-box"><!--  如果有折扣展示折扣价原价  --><div v-if="course.discount_type"><span class="discount-type">{{ course.discount_type }}</span><span class="discount-price">¥{{ course.real_price }}元</span><span class="original-price">原价:{{ course.price }}元</span></div><!--  否则直接展示原价  --><span v-else class="discount-price">¥{{ course.price }}元</span><span class="buy-now">立即购买</span></div></div></div></div><!--  分页器  --><div class="course_pagination block"><el-pagination@size-change="handleSizeChange"@current-change="handleCurrentChange":current-page.sync="filter.page":page-sizes="[2, 3, 5, 10]":page-size="filter.page_size"layout="sizes, prev, pager, next":total="course_total"></el-pagination></div></div><!-- 尾部组件 --><Footer></Footer></div>
</template><script>
// 导入头部组件
import Header from "@/components/Head"
import Footer from "@/components/Footer"export default {// 课程页面组件名称name: "Course",data() {return {category_list: [],  // 课程分类列表course_list: [],  // 课程列表course_total: 0,  // 当前课程的总数量filter: {course_category: 0,  // 当前用户选择的课程分类,刚进入页面默认为全部,值为0ordering: "-id",  // 数据的排序方式,默认值是-id,表示对于id进行降序排列page_size: 2,  // 单页数据量page: 1,}}},// 生命周期钩子函数created() {// 获取分类this.get_category();// 获取课程this.get_course();},components: {// 使用的子组件Header,Footer,},// 监听变量, 当变量变化的时候, 方法watch: {"filter.course_category": function () {this.filter.page = 1;this.get_course();},"filter.ordering": function () {this.get_course();},"filter.page_size": function () {this.get_course();},"filter.page": function () {this.get_course();}},methods: {// 页码器方法handleSizeChange(val) {// 每页数据量发生变化时执行的方法this.filter.page = 1;this.filter.page_size = val;},// 页码器方法handleCurrentChange(val) {// 页码发生变化时执行的方法this.filter.page = val;},// 获取课程分类信息get_category() {this.$axios.get(`${this.$settings.base_url}/course/categories/`).then(response => {this.category_list = response.data.data;}).catch(() => {this.$message({message: "获取课程分类信息有误,请联系客服工作人员",})})},// 获取课程信息get_course() {// 排序let filters = {ordering: this.filter.ordering, // 排序};// 判决是否进行分类课程的展示if (this.filter.course_category > 0) {filters.course_category = this.filter.course_category;}// 设置单页数据量if (this.filter.page_size > 0) {filters.page_size = this.filter.page_size;} else {filters.page_size = 5;}// 设置当前页码if (this.filter.page > 1) {filters.page = this.filter.page;} else {filters.page = 1;}// 只要修改filters的数据则可以设置排序过滤// 获取课程列表信息this.$axios.get(`${this.$settings.base_url}/course/free/`, {params: filters}).then(response => {// console.log(response.data.data);this.course_list = response.data.data.results;this.course_total = response.data.data.count;console.log(this.course_list);}).catch(() => {this.$message({message: "获取课程信息有误,请联系客服工作人员"})})}}
}
</script><style scoped>
.course {background: #f6f6f6;
}.course .main {width: 1100px;margin: 35px auto 0;
}.course .condition {margin-bottom: 35px;padding: 25px 30px 25px 20px;background: #fff;border-radius: 4px;box-shadow: 0 2px 4px 0 #f0f0f0;
}.course .cate-list {border-bottom: 1px solid #333;border-bottom-color: rgba(51, 51, 51, .05);padding-bottom: 18px;margin-bottom: 17px;
}.course .cate-list::after {content: "";display: block;clear: both;
}.course .cate-list li {float: left;font-size: 16px;padding: 6px 15px;line-height: 16px;margin-left: 14px;position: relative;transition: all .3s ease;cursor: pointer;color: #4a4a4a;border: 1px solid transparent; /* transparent 透明 */
}.course .cate-list .title {color: #888;margin-left: 0;letter-spacing: .36px;padding: 0;line-height: 28px;
}.course .cate-list .this {color: #ffc210;border: 1px solid #ffc210 !important;border-radius: 30px;
}.course .ordering::after {content: "";display: block;clear: both;
}.course .ordering ul {float: left;
}.course .ordering ul::after {content: "";display: block;clear: both;
}.course .ordering .condition-result {float: right;font-size: 14px;color: #9b9b9b;line-height: 28px;
}.course .ordering ul li {float: left;padding: 6px 15px;line-height: 16px;margin-left: 14px;position: relative;transition: all .3s ease;cursor: pointer;color: #4a4a4a;
}.course .ordering .title {font-size: 16px;color: #888;letter-spacing: .36px;margin-left: 0;padding: 0;line-height: 28px;
}.course .ordering .this {color: #ffc210;
}.course .ordering .price {position: relative;
}.course .ordering .price::before,
.course .ordering .price::after {cursor: pointer;content: "";display: block;width: 0;height: 0;border: 5px solid transparent;position: absolute;right: 0;
}.course .ordering .price::before {border-bottom: 5px solid #aaa;margin-bottom: 2px;top: 2px;
}.course .ordering .price::after {border-top: 5px solid #aaa;bottom: 2px;
}.course .ordering .price_up::before {border-bottom-color: #ffc210;
}.course .ordering .price_down::after {border-top-color: #ffc210;
}.course .course-item:hover {box-shadow: 4px 6px 16px rgba(0, 0, 0, .5);
}.course .course-item {width: 1100px;background: #fff;padding: 20px 30px 20px 20px;margin-bottom: 35px;border-radius: 2px;cursor: pointer;box-shadow: 2px 3px 16px rgba(0, 0, 0, .1);/* css3.0 过渡动画 hover 事件操作 */transition: all .2s ease;
}.course .course-item::after {content: "";display: block;clear: both;
}/* 顶级元素 父级元素  当前元素{} */
.course .course-item .course-image {float: left;width: 423px;height: 210px;margin-right: 30px;
}.course .course-item .course-image img {max-width: 100%;max-height: 210px;
}.course .course-item .course-info {float: left;width: 596px;
}.course-item .course-info h3 a {font-size: 26px;color: #333;font-weight: normal;margin-bottom: 8px;
}.course-item .course-info h3 span {font-size: 14px;color: #9b9b9b;float: right;margin-top: 14px;
}.course-item .course-info h3 span img {width: 11px;height: auto;margin-right: 7px;
}.course-item .course-info .teacher-info {font-size: 14px;color: #9b9b9b;margin-top: 1px;margin-bottom: 14px;padding-bottom: 14px;border-bottom: 1px solid #333;border-bottom-color: rgba(51, 51, 51, .05);
}.course-item .course-info .teacher-info span {float: right;
}.course-item .section-list::after {content: "";display: block;clear: both;
}.course-item .section-list li {float: left;width: 44%;font-size: 14px;color: #666;padding-left: 22px;/* background: url("路径") 是否平铺 x轴位置 y轴位置 */background: url("/src/assets/img/play-icon-gray.svg") no-repeat left 4px;margin-bottom: 15px;
}.course-item .section-list li .section-title {/* 以下3句,文本内容过多,会自动隐藏,并显示省略符号 */text-overflow: ellipsis;overflow: hidden;white-space: nowrap;display: inline-block;max-width: 200px;
}.course-item .section-list li:hover {background-image: url("/src/assets/img/play-icon-yellow.svg");color: #ffc210;
}.course-item .section-list li .free {width: 34px;height: 20px;color: #fd7b4d;vertical-align: super;margin-left: 10px;border: 1px solid #fd7b4d;border-radius: 2px;text-align: center;font-size: 13px;white-space: nowrap;
}.course-item .section-list li:hover .free {color: #ffc210;border-color: #ffc210;
}.course-item {position: relative;
}.course-item .pay-box {position: absolute;bottom: 20px;width: 600px;
}.course-item .pay-box::after {content: "";display: block;clear: both;
}.course-item .pay-box .discount-type {padding: 6px 10px;font-size: 16px;color: #fff;text-align: center;margin-right: 8px;background: #fa6240;border: 1px solid #fa6240;border-radius: 10px 0 10px 0;float: left;
}.course-item .pay-box .discount-price {font-size: 24px;color: #fa6240;float: left;
}.course-item .pay-box .original-price {text-decoration: line-through;font-size: 14px;color: #9b9b9b;margin-left: 10px;float: left;margin-top: 10px;
}.course-item .pay-box .buy-now {width: 120px;height: 38px;background: transparent;color: #fa6240;font-size: 16px;border: 1px solid #fd7b4d;border-radius: 3px;transition: all .2s ease-in-out;float: right;text-align: center;line-height: 38px;position: absolute;right: 0;bottom: 5px;
}.course-item .pay-box .buy-now:hover {color: #fff;background: #ffc210;border: 1px solid #ffc210;
}.course .course_pagination {margin-bottom: 60px;text-align: center;
}
</style>

5. 课程详情单查接口

在群查接口上添加RetrieveModelMixin视图子类即可.
# 导入单查视图子类
from rest_framework.mixins import RetrieveModelMixin
# 课程接口
class CourseView(GenericViewSet, ListModelMixin, RetrieveModelMixin):...
测试地址: http://127.0.0.1:8000/course/free/1

6. 课程的课时接口

过滤出某个课程的所有课时.
* 1. 路由
# 课时接口
router.register('chapters', views.CourseSectionsView, 'chapters')
* 2. 模型序列化类
# 课时表模型序列化器
class CourseSectionModelSerializer(serializers.ModelSerializer):class Meta:model = models.CourseSection# 课时的名字  课时排序 视频时长 是否可以试看 课时链接, 课程类型fields = ['name', 'orders', 'duration', 'free_trail', 'section_link', 'section_type_name']# 课程章节模型序列化器
class CourseChapterModelSerializer(serializers.ModelSerializer):# 子序列化方法获取导师表的数据, course_sections字段# 对应CourseChapter表chapter字段的related_name='course_sections'# 通过course_sections去访问CourseSection表数据# 对应的值有多个 设置many=Truecourse_sections = CourseSectionModelSerializer(many=True)class Meta:model = models.CourseChapter# 第几章  章节介绍  章节名称 课时信息fields = ['name', 'summary', 'chapter', 'course_sections']
* 3. 视图类
# 查询所有课时接口
class CourseSectionsView(GenericViewSet, ListModelMixin):# 使用课程表模型queryset = models.CourseChapter.objects.filter(is_delete=False, is_show=True)# 使用的序列化器serializer_class = serializer.CourseChapterModelSerializer# 导入django-filters过滤器from django_filters.rest_framework import DjangoFilterBackend# 配置过滤和排序配置filter_backends = [DjangoFilterBackend]# 视图类中指定django-filters的过滤字段, 通过外键course获取某个课程的所有课时filter_fields = ['course']
* 4. 测试地址: http://127.0.0.1:8000/course/chapters/?course=pk值http://127.0.0.1:8000/course/chapters/?course=1

7. 课程详情前台

* 1. 新建页面组件在src/views/下新建FreeCourseDetail.vue文件
<template><div class="detail"><!-- 头部主键 --><Header/><!-- 课程详情页面 --><div class="main"><!-- 课程信息 --><div class="course-info"><!-- 左边是视频 --><div class="wrap-left"><!--使用视频组件--><videoPlayer class="video-player vjs-custom-skin"ref="videoPlayer":playsinline="true":options="playerOptions"@play="onPlayerPlay($event)"@pause="onPlayerPause($event)"></videoPlayer></div><!-- 右边是课程信息 --><div class="wrap-right"><!--课程标签--><h3 class="course-name">{{ course_info.name }}</h3><!--课时--><p class="data">{{ course_info.students }}人在学&nbsp;&nbsp;&nbsp;&nbsp;课程总时长:{{course_info.sections}}课时/{{ course_info.pub_sections }}小时&nbsp;&nbsp;&nbsp;&nbsp;难度:{{ course_info.level_name }}</p><!--课程价格--><div class="sale-time"><p class="sale-type">价格 <span class="original_price">¥{{ course_info.price }}</span></p><p class="expire"></p></div><!--课程购买--><div class="buy"><div class="buy-btn"><button class="buy-now">立即购买</button><button class="free">免费试学</button></div><!--<div class="add-cart" @click="add_cart(course_info.id)">--><!--<img src="@/assets/img/cart-yellow.svg" alt="">加入购物车--><!--</div>--></div></div></div><!-- 课程标签 --><div class="course-tab"><ul class="tab-list"><!--在点击 详情介绍 时, 将tabIndex的值设置为1, 并使用active样式选中--><li :class="tabIndex==1?'active':''" @click="tabIndex=1">详情介绍</li><!--在点击 课程章节 时, 将tabIndex的值设置为2, 并使用active样式选中, 如果不选中的时候试学 使用文字颜色为橙色 --><li :class="tabIndex==2?'active':''" @click="tabIndex=2">课程章节 <span :class="tabIndex!=2?'free':''">(试学)</span></li><li :class="tabIndex==3?'active':''" @click="tabIndex=3">用户评论</li><li :class="tabIndex==4?'active':''" @click="tabIndex=4">常见问题</li></ul></div><!-- 课程信息 --><div class="course-content"><!-- 课程标签列表 --><div class="course-tab-list"><!-- 课程详情内容 --><div class="tab-item" v-if="tabIndex==1"><div class="course-brief" v-html="course_info.brief_text"></div></div><!-- 课程章节内容 --><div class="tab-item" v-if="tabIndex==2"><div class="tab-item-title"><p class="chapter">课程章节</p><p class="chapter-length">共{{ course_chapters.length }}章 {{ course_info.sections }}个课时</p></div><div class="chapter-item" v-for="chapter in course_chapters" :key="chapter.name"><p class="chapter-title"><img src="@/assets/img/enum.svg"alt="">第{{ chapter.chapter }}章·{{ chapter.name }}</p><ul class="section-list"><li class="section-item" v-for="section in chapter.coursesections" :key="section.name"><p class="name"><span class="index">{{ chapter.chapter }}-{{ section.orders }}</span>{{ section.name }}<span class="free" v-if="section.free_trail">免费</span></p><p class="time">{{ section.duration }} <img src="@/assets/img/chapter-player.svg"></p><button class="try" v-if="section.free_trail">立即试学</button><button class="try" v-else>立即购买</button></li></ul></div></div><!-- 用户评论内容 --><div class="tab-item" v-if="tabIndex==3">用户评论</div><!-- 常见问题内容 --><div class="tab-item" v-if="tabIndex==4">常见问题</div></div><!--课程导师介绍--><div class="course-side"><!-- 导师信息区域  --><div class="teacher-info"><!--授课老师标题--><h4 class="side-title"><span>授课老师</span></h4><!--导师信息--><div class="teacher-content"><div class="cont1"><!-- 导师头像 --><img :src="course_info.teacher.image"><div class="name"><!-- 导师名字 --><p class="teacher-name">{{ course_info.teacher.name }}<!--导师职位-->{{ course_info.teacher.title }}</p><!--导师签名--><p class="teacher-title">{{ course_info.teacher.signature }}</p></div></div><!--导师描述--><p class="narrative">{{ course_info.teacher.brief }}</p></div></div></div></div></div><!-- 底部主键 --><Footer/></div>
</template><script>
import Header from "@/components/Head"
import Footer from "@/components/Footer"// 加载视频组件
import {videoPlayer} from 'vue-video-player';export default {name: "FreeCourseDetail",data() {return {tabIndex: 2,  // 当前选项卡显示的下标course_id: 0,  // 当前课程信息的IDcourse_info: {teacher: {},}, // 课程信息course_chapters: [],  // 课程的章节课时列表// 播放器配置playerOptions: {aspectRatio: '16:9',  // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3")sources: [{  // 播放资源和资源格式type: "video/mp4",src: "" //你的视频地址(必填)}],}}},// 生命周期钩子函数created() {// 获取课程编号this.get_course_id();// 获取课程数据this.get_course_data();// 获取章节this.get_chapter();},methods: {onPlayerPlay() {// 当视频播放时,执行的方法console.log('视频开始播放')},onPlayerPause() {// 当视频暂停播放时,执行的方法console.log('视频暂停,可以打开广告了')},get_course_id() {// 获取地址栏上面的课程ID, this.$route.query.pk获取不了使用 this.$route.query.pk的方式去获取this.course_id = this.$route.params.pk || this.$route.query.pk;// id小于1直接报错if (this.course_id < 1) {let _this = this;_this.$alert("对不起,当前视频不存在!", "警告", {callback() {_this.$router.go(-1);}});}},// 获取单个课程信息get_course_data() {// ajax请求课程信息this.$axios.get(`${this.$settings.base_url}/course/free/${this.course_id}/`).then(response => {// window.console.log(response.data);this.course_info = response.data;console.log(this.course_info)}).catch(() => {this.$message({message: "对不起,访问页面出错!请联系客服工作人员!"});})},// 获取当前课程的课时get_chapter() {// 获取当前课程对应的章节课时信息// http://127.0.0.1:8000/course/chapters/?course=(pk)this.$axios.get(`${this.$settings.base_url}/course/chapters/`, {params: {"course": this.course_id,}}).then(response => {this.course_chapters = response.data;}).catch(error => {window.console.log(error.response);})},},components: {Header,Footer,videoPlayer, // 注册视频组件}
}
</script><style scoped>
.main {background: #fff;padding-top: 30px;
}.course-info {width: 1200px;margin: 0 auto;overflow: hidden;
}.wrap-left {float: left;width: 690px;height: 388px;background-color: #000;
}.wrap-right {float: left;position: relative;height: 388px;
}.course-name {font-size: 20px;color: #333;padding: 10px 23px;letter-spacing: .45px;
}.data {padding-left: 23px;padding-right: 23px;padding-bottom: 16px;font-size: 14px;color: #9b9b9b;
}.sale-time {width: 464px;background: #fa6240;font-size: 14px;color: #4a4a4a;padding: 10px 23px;overflow: hidden;
}.sale-type {font-size: 16px;color: #fff;letter-spacing: .36px;float: left;
}.sale-time .expire {font-size: 14px;color: #fff;float: right;
}.sale-time .expire .second {width: 24px;display: inline-block;background: #fafafa;color: #5e5e5e;padding: 6px 0;text-align: center;
}.course-price {background: #fff;font-size: 14px;color: #4a4a4a;padding: 5px 23px;
}.discount {font-size: 26px;color: #fa6240;margin-left: 10px;display: inline-block;margin-bottom: -5px;
}.original {font-size: 14px;color: #9b9b9b;margin-left: 10px;text-decoration: line-through;
}.buy {width: 464px;padding: 0px 23px;position: absolute;left: 0;bottom: 20px;overflow: hidden;
}.buy .buy-btn {float: left;
}.buy .buy-now {width: 125px;height: 40px;border: 0;background: #ffc210;border-radius: 4px;color: #fff;cursor: pointer;margin-right: 15px;outline: none;
}.buy .free {width: 125px;height: 40px;border-radius: 4px;cursor: pointer;margin-right: 15px;background: #fff;color: #ffc210;border: 1px solid #ffc210;
}.add-cart {float: right;font-size: 14px;color: #ffc210;text-align: center;cursor: pointer;margin-top: 10px;
}.add-cart img {width: 20px;height: 18px;margin-right: 7px;vertical-align: middle;
}.course-tab {width: 100%;background: #fff;margin-bottom: 30px;box-shadow: 0 2px 4px 0 #f0f0f0;}.course-tab .tab-list {width: 1200px;margin: auto;color: #4a4a4a;overflow: hidden;
}.tab-list li {float: left;margin-right: 15px;padding: 26px 20px 16px;font-size: 17px;cursor: pointer;
}.tab-list .active {color: #ffc210;border-bottom: 2px solid #ffc210;
}.tab-list .free {color: #fb7c55;
}.course-content {width: 1200px;margin: 0 auto;background: #FAFAFA;overflow: hidden;padding-bottom: 40px;
}.course-tab-list {width: 880px;height: auto;padding: 20px;background: #fff;float: left;box-sizing: border-box;overflow: hidden;position: relative;box-shadow: 0 2px 4px 0 #f0f0f0;
}.tab-item {width: 880px;background: #fff;padding-bottom: 20px;box-shadow: 0 2px 4px 0 #f0f0f0;
}.tab-item-title {justify-content: space-between;padding: 25px 20px 11px;border-radius: 4px;margin-bottom: 20px;border-bottom: 1px solid #333;border-bottom-color: rgba(51, 51, 51, .05);overflow: hidden;
}.chapter {font-size: 17px;color: #4a4a4a;float: left;
}.chapter-length {float: right;font-size: 14px;color: #9b9b9b;letter-spacing: .19px;
}.chapter-title {font-size: 16px;color: #4a4a4a;letter-spacing: .26px;padding: 12px;background: #eee;border-radius: 2px;display: -ms-flexbox;display: flex;-ms-flex-align: center;align-items: center;
}.chapter-title img {width: 18px;height: 18px;margin-right: 7px;vertical-align: middle;
}.section-list {padding: 0 20px;
}.section-list .section-item {padding: 15px 20px 15px 36px;cursor: pointer;justify-content: space-between;position: relative;overflow: hidden;
}.section-item .name {font-size: 14px;color: #666;float: left;
}.section-item .index {margin-right: 5px;
}.section-item .free {font-size: 12px;color: #fff;letter-spacing: .19px;background: #ffc210;border-radius: 100px;padding: 1px 9px;margin-left: 10px;
}.section-item .time {font-size: 14px;color: #666;letter-spacing: .23px;opacity: 1;transition: all .15s ease-in-out;float: right;
}.section-item .time img {width: 18px;height: 18px;margin-left: 15px;vertical-align: text-bottom;
}.section-item .try {width: 86px;height: 28px;background: #ffc210;border-radius: 4px;font-size: 14px;color: #fff;position: absolute;right: 20px;top: 10px;opacity: 0;transition: all .2s ease-in-out;cursor: pointer;outline: none;border: none;
}.section-item:hover {background: #fcf7ef;box-shadow: 0 0 0 0 #f3f3f3;
}.section-item:hover .name {color: #333;
}.section-item:hover .try {opacity: 1;
}.course-side {width: 300px;height: auto;margin-left: 20px;float: right;
}.teacher-info {background: #fff;margin-bottom: 20px;box-shadow: 0 2px 4px 0 #f0f0f0;
}.side-title {font-weight: normal;font-size: 17px;color: #4a4a4a;padding: 18px 14px;border-bottom: 1px solid #333;border-bottom-color: rgba(51, 51, 51, .05);
}.side-title span {display: inline-block;border-left: 2px solid #ffc210;padding-left: 12px;
}.teacher-content {padding: 30px 20px;box-sizing: border-box;
}.teacher-content .cont1 {margin-bottom: 12px;overflow: hidden;
}.teacher-content .cont1 img {width: 54px;height: 54px;margin-right: 12px;float: left;
}.teacher-content .cont1 .name {float: right;
}.teacher-content .cont1 .teacher-name {width: 188px;font-size: 16px;color: #4a4a4a;padding-bottom: 4px;
}.teacher-content .cont1 .teacher-title {width: 188px;font-size: 13px;color: #9b9b9b;white-space: nowrap;
}.teacher-content .narrative {font-size: 14px;color: #666;line-height: 24px;
}
</style>
* 2. 安装视频组件命令: cnpm install vue-video-player

* 3. 在main.js中配置视图组件
// vue-video播放器
require('video.js/dist/video-js.css');
require('vue-video-player/src/custom-theme.css');
import VideoPlayer from 'vue-video-player'Vue.use(VideoPlayer);
* 4. 在src/router/index.js 路由文件中添加FreeCourseDetail页面的路由.
// 导入免费课程详情页面
import FreeCourseDetail from "@/views/FreeCourseDetail"// 免费课程页面详情
{path: '/free/detail/:id',name: 'FreeCourseDetail',component: FreeCourseDetail
},
:id 将路由之后的值赋值给id, 可以通过this.$route.params.id 获取值.
:名称 --> .名称

8. 视图托管

* 1. 注册七牛云https://portal.qiniu.com/
* 2. 注册之后再个人中心实名认证
* 3. 空间管理中新建空间, 访问权限设置为公开即可.

* 2. 上传视频到空间

* 3. 复制文件的连接地址

* 4. 免费课程详情页面中的视频的地址.
<script>
export default {name: "FreeCourseDetail",data() {return {playerOptions: {...src: "http://rcux5weit.hn-bkt.clouddn.com/bear.mp4?e=1654189473&token=XzxkEBU9Taah6BwPBiF5CcJLP4nbQ-dpJmesiGOq:F4ihGlpjjWgQKPs6CAAipL7bAdk="...
* 5. 查看效果

9. 视频广告简介

<!--使用视频组件-->
<videoPlayer class="video-player vjs-custom-skin"ref="videoPlayer":playsinline="true":options="playerOptions"@play="onPlayerPlay($event)"@pause="onPlayerPause($event)">
</videoPlayer>
methods: {onPlayerPlay() {// 当视频播放时,执行的方法console.log('视频开始播放')
},onPlayerPause() {// 当视频暂停播放时,执行的方法console.log('视频暂停,可以打开广告了')
},
...

10. 按课程名称搜索接口

ps: 全文检索引擎: elasticsearch, java封装的一个数据库, 专注于大数据存储和搜索.
* 1. 路由
# 搜索接口
router.register('search', views.CouresSearchView, 'search')
* 2. 视图类
# 简单搜索功能
class CourseSearchView(GenericViewSet, ListModelMixin):# 使用课程表模型queryset = models.Course.objects.filter(is_delete=False, is_show=True)# 使用的序列化器serializer_class = serializer.CourseModelSerializer# 导入分页器from .paginations import PageNumberPaginationpagination_class = PageNumberPagination# 导入内置的过滤模块, 支持模糊查询from rest_framework.filters import SearchFilter# 配置过滤和排序配置filter_backends = [SearchFilter]# 过滤的字段search_fields = ['name']
* 3. 测试地址: http://127.0.0.1:8000/course/search/?search=linuxhttp://127.0.0.1:8000/course/search/ 查询全部数据http://127.0.0.1:8000/course/search/?search=课程名称存在的文字

11. 搜索页面前台

this.$route.params 从路径中取值,   对应 --> :变量
this.$router.query 从路径?后面取值 对应 --> ?key=valueurl地址: 127.0.0.1:8000/路径?xx=xx
* 1. 新建搜索详情页面组件 SearchCourse.vue
<template><div class="search-course course"><Header/><!-- 课程列表 --><div class="main"><!-- 搜索结果大于0展示课程列表 --><div v-if="course_list.length > 0" class="course-list"><div class="course_total">搜索到{{course_total}}条结果</div><!-- 遍历课程列表 --><div class="course-item" v-for="course in course_list" :key="course.name"><div class="course-image"><!--  课程封面  --><img :src="course.course_img" alt=""></div><div class="course-info"><h3><!--  课程名称 点击课程跳转到课程详情 --><router-link :to="'/free/detail/'+course.id">{{ course.name }}</router-link><!--  学习人数  --><span><img src="@/assets/img/avatar1.svg" alt="">{{ course.students }}人已加入学习</span></h3><p class="teacher-info"><!-- 导师姓名 职位 签名 -->{{ course.teacher.name }} {{ course.teacher.title }} {{ course.teacher.signature }}<!-- 课时更新展示 --><span v-if="course.sections>course.pub_sections">共{{ course.sections }}课时/已更新{{course.pub_sections}}课时</span><span v-else>共{{ course.sections }}课时/更新完成</span></p><!--  课时展示  --><ul class="section-list"><!-- key从o开始 从1开始则 key+1  --><li v-for="(section, key) in course.section_list" :key="section.name"><span class="section-title">0{{ key + 1 }}  |  {{ section.name }}</span><!--如果课程免费, 展示免费字样--><span class="free" v-if="section.free_trail">免费</span></li></ul><div class="pay-box"><!--  如果有折扣展示折扣价原价  --><div v-if="course.discount_type"><span class="discount-type">{{ course.discount_type }}</span><span class="discount-price">¥{{ course.real_price }}元</span><span class="original-price">原价:{{ course.price }}元</span></div><!--  否则直接展示原价  --><span v-else class="discount-price">¥{{ course.price }}元</span><span class="buy-now">立即购买</span></div></div></div></div><div v-else style="text-align: center; line-height: 60px">没有搜索结果</div><div class="course_pagination block"><el-pagination@size-change="handleSizeChange"@current-change="handleCurrentChange":current-page.sync="filter.page":page-sizes="[2, 3, 5, 10]":page-size="filter.page_size"layout="sizes, prev, pager, next":total="course_total"></el-pagination></div></div></div>
</template><script>
import Header from '../components/Head'export default {name: "SearchCourse",components: {Header,},data() {return {// 课程列表course_list: [],// 搜索结果数量course_total: 0,// 分页器参数filter: {page_size: 10,  // 每页展示10条page: 1,  // 默认展示第一页search: '',  // 检索的关键字}}},// 生命周期钩子函数created() {// 获取课程信息this.get_course()},// 监听路由是否发生变化// 除了在搜索框中输入搜索的数据, 触发获取课程信息, 好可以监听地址栏的变化触发获取课程信息watch: {'$route.query'() {this.get_course()}},methods: {// 每页数据量发生变化时执行的方法handleSizeChange(val) {this.filter.page = 1;this.filter.page_size = val;},// 页码发生变化时执行的方法handleCurrentChange(val) {this.filter.page = val;},// 获取课程数据get_course() {// 获取搜索的关键字, 支持?word=xx 与 ?wd=xxthis.filter.search = this.$route.query.word || this.$route.query.wd;// 获取课程列表信息 向 http:127.0.0.1:8000/course/search/?page_size=10&page=1&search=xxx 发送请求this.$axios.get(`${this.$settings.base_url}/course/search/`, {params: this.filter}).then(response => {// 如果后台不分页,数据在response.data中;如果后台分页,数据在response.data.results中this.course_list = response.data.results;// 获取数据的总数this.course_total = response.data.count;}).catch(() => {this.$message({message: "获取课程信息有误,请联系客服工作人员"})})}}
}
</script><style scoped>
.course {background: #f6f6f6;
}.course .main {width: 1100px;margin: 35px auto 0;
}.course .condition {margin-bottom: 35px;padding: 25px 30px 25px 20px;background: #fff;border-radius: 4px;box-shadow: 0 2px 4px 0 #f0f0f0;
}.course .cate-list {border-bottom: 1px solid #333;border-bottom-color: rgba(51, 51, 51, .05);padding-bottom: 18px;margin-bottom: 17px;
}.course .cate-list::after {content: "";display: block;clear: both;
}.course .cate-list li {float: left;font-size: 16px;padding: 6px 15px;line-height: 16px;margin-left: 14px;position: relative;transition: all .3s ease;cursor: pointer;color: #4a4a4a;border: 1px solid transparent; /* transparent 透明 */
}.course .cate-list .title {color: #888;margin-left: 0;letter-spacing: .36px;padding: 0;line-height: 28px;
}.course .cate-list .this {color: #ffc210;border: 1px solid #ffc210 !important;border-radius: 30px;
}.course .ordering::after {content: "";display: block;clear: both;
}.course .ordering ul {float: left;
}.course .ordering ul::after {content: "";display: block;clear: both;
}.course .ordering .condition-result {float: right;font-size: 14px;color: #9b9b9b;line-height: 28px;
}.course .ordering ul li {float: left;padding: 6px 15px;line-height: 16px;margin-left: 14px;position: relative;transition: all .3s ease;cursor: pointer;color: #4a4a4a;
}.course .ordering .title {font-size: 16px;color: #888;letter-spacing: .36px;margin-left: 0;padding: 0;line-height: 28px;
}.course .ordering .this {color: #ffc210;
}.course .ordering .price {position: relative;
}.course .ordering .price::before,
.course .ordering .price::after {cursor: pointer;content: "";display: block;width: 0px;height: 0px;border: 5px solid transparent;position: absolute;right: 0;
}.course .ordering .price::before {border-bottom: 5px solid #aaa;margin-bottom: 2px;top: 2px;
}.course .ordering .price::after {border-top: 5px solid #aaa;bottom: 2px;
}.course .ordering .price_up::before {border-bottom-color: #ffc210;
}.course .ordering .price_down::after {border-top-color: #ffc210;
}.course .course-item:hover {box-shadow: 4px 6px 16px rgba(0, 0, 0, .5);
}.course .course-item {width: 1100px;background: #fff;padding: 20px 30px 20px 20px;margin-bottom: 35px;border-radius: 2px;cursor: pointer;box-shadow: 2px 3px 16px rgba(0, 0, 0, .1);/* css3.0 过渡动画 hover 事件操作 */transition: all .2s ease;
}.course .course-item::after {content: "";display: block;clear: both;
}/* 顶级元素 父级元素  当前元素{} */
.course .course-item .course-image {float: left;width: 423px;height: 210px;margin-right: 30px;
}.course .course-item .course-image img {max-width: 100%;max-height: 210px;
}.course .course-item .course-info {float: left;width: 596px;
}.course-item .course-info h3 a {font-size: 26px;color: #333;font-weight: normal;margin-bottom: 8px;
}.course-item .course-info h3 span {font-size: 14px;color: #9b9b9b;float: right;margin-top: 14px;
}.course-item .course-info h3 span img {width: 11px;height: auto;margin-right: 7px;
}.course-item .course-info .teacher-info {font-size: 14px;color: #9b9b9b;margin-top: 2px;margin-bottom: 14px;padding-bottom: 14px;border-bottom: 1px solid #333;border-bottom-color: rgba(51, 51, 51, .05);
}.course-item .course-info .teacher-info span {float: right;
}.course-item .section-list::after {content: "";display: block;clear: both;
}.course-item .section-list li {float: left;width: 44%;font-size: 14px;color: #666;padding-left: 22px;/* background: url("路径") 是否平铺 x轴位置 y轴位置 */background: url("/src/assets/img/play-icon-gray.svg") no-repeat left 4px;margin-bottom: 15px;
}.course-item .section-list li .section-title {/* 以下3句,文本内容过多,会自动隐藏,并显示省略符号 */text-overflow: ellipsis;overflow: hidden;white-space: nowrap;display: inline-block;max-width: 200px;
}.course-item .section-list li:hover {background-image: url("/src/assets/img/play-icon-yellow.svg");color: #ffc210;
}.course-item .section-list li .free {width: 34px;height: 20px;color: #fd7b4d;vertical-align: super;margin-left: 10px;border: 1px solid #fd7b4d;border-radius: 2px;text-align: center;font-size: 13px;white-space: nowrap;
}.course-item .section-list li:hover .free {color: #ffc210;border-color: #ffc210;
}.course-item {position: relative;
}.course-item .pay-box {position: absolute;bottom: 20px;width: 600px;
}.course-item .pay-box::after {content: "";display: block;clear: both;
}.course-item .pay-box .discount-type {padding: 6px 10px;font-size: 16px;color: #fff;text-align: center;margin-right: 8px;background: #fa6240;border: 1px solid #fa6240;border-radius: 10px 0 10px 0;float: left;
}.course-item .pay-box .discount-price {font-size: 24px;color: #fa6240;float: left;
}.course-item .pay-box .original-price {text-decoration: line-through;font-size: 14px;color: #9b9b9b;margin-left: 10px;float: left;margin-top: 10px;
}.course-item .pay-box .buy-now {width: 120px;height: 38px;background: transparent;color: #fa6240;font-size: 16px;border: 1px solid #fd7b4d;border-radius: 3px;transition: all .2s ease-in-out;float: right;text-align: center;line-height: 38px;position: absolute;right: 0;bottom: 5px;
}.course-item .pay-box .buy-now:hover {color: #fff;background: #ffc210;border: 1px solid #ffc210;
}.course .course_pagination {margin-bottom: 60px;text-align: center;
}
.course .course_total {text-align: center;font-size: 24px;color: #fa6240;margin-bottom: 10px;
}</style>
* 2. src/router/index.js中配置路由
// 导入搜索课程页面
import Search from "@/views/Search"// 搜索课程页面
{path: '/course/search/',name: 'Search',component: Search
},
* 3. head组件添加一下代码
<template><!-- 右侧导航条 --><div class="right-part">...</div><!--搜索框--><form class="search"><div class="tips" v-if="is_search_tip"><!--点击Python是触发 search_action函数将Python作为参数进行传递--><span @click="search_action('Python')">Python</span><span @click="search_action('Linux')">Linux</span></div><input type="text" :placeholder="search_placeholder" @focus="on_search" @blur="off_search"v-model="search_word"><button type="button" class="glyphicon glyphicon-search" @click="search_action(search_word)"></button></form>
</template><script>export default {data() {return {// 搜索栏数据// 是否展示 默认的Python Linux 关键字is_search_tip: true,// 搜索占位符search_placeholder: '',// 输入的搜索关键字search_word: '', }},methods: {// 搜索search_action(search_word) {// 如果search_word参数的值为空提示输入搜索的内容, 否则直接返回if (!search_word) {this.$message('请输入要搜索的内容');return}// this.$route.query.word 获取?后面word的参数// 输入的数据 != 路由?后面的word的值, 则进行路由跳转, 值相等就什么都不做if (search_word !== this.$route.query.word) {// 跳转到 http:127.0.0.1:8000/course/search?word=关键this.$router.push(`/course/search?word=${search_word}`);}// 将输入的搜索次清空this.search_word = '';},// 输入框聚焦的时候展示的内容, 不展示默认的Python 与 Linuxon_search() {this.search_placeholder = '请输入想搜索的课程';this.is_search_tip = false;},// 输入框失去焦点的时候, 将占位符设置为''  并展示 Python 与 Linux关键字off_search() {this.search_placeholder = '';this.is_search_tip = true;},}
</script><style scoped>.search {float: right;position: relative;margin-top: 22px;margin-right: 10px;}.search input, .search button {border: none;outline: none;background-color: white;}.search input {border-bottom: 1px solid #eeeeee;}.search input:focus {border-bottom-color: orange;}.search input:focus + button {color: orange;}.search .tips {position: absolute;bottom: 3px;left: 0;}.search .tips span {border-radius: 11px;background-color: #eee;line-height: 22px;display: inline-block;padding: 0 7px;margin-right: 3px;cursor: pointer;color: #aaa;font-size: 14px;}.search .tips span:hover {color: orange;}
</style>
* 4. 点击默认关键字跳转到 跳转到 http://localhost:8080/#/course/search?word=Linux

if (search_word !== this.$route.query.word) {// 跳转到 http:127.0.0.1:8000/course/search?word=关键this.$router.push(`/course/search?word=${search_word}`);}
// 输入或通过点击, 拼接的路由地址, 与当前地址一样, 则什么都不做!!!
* 5. 测试(需要点击搜索图标才能搜索.)点击搜索的时候是得到一个vue页面, vue页面通过生命周期钩子函数发送了ajax请求.生命周期钩子通过this.$router.query 获取路径?后面取值 拼接成: http:127.0.0.1:8000/course/search/?page_size=10&page=1&search=Linux 向后端发送请求.

4.学城项目 课程接口相关推荐

  1. Python高级全栈开发实战 老男孩课程S16+路飞学城项目+女神串讲 Python全栈直通车课程

    python高级全栈开发实战 老男孩课程S16+路飞学城项目+女神串讲 Python全栈直通车课程 Python高级全栈开发实战老男孩课程,是可以帮助同学们从零基础开始到项目开发实战的全栈课程,内容非 ...

  2. 路飞学城项目介绍与分析

    路飞学城项目介绍与分析 文章目录 路飞学城项目介绍与分析 一.企业的web项目类型 二.企业项目开发流程 三.立项申请阶段 四.需求分析 1. 首页 2.注册登录 3.课程列表 4.课程详情 5.购物 ...

  3. 0. 学城项目 后端环境配置

    1. 企业的web项目类型 1. 商城 (某某团购)1.1 B2C直销商城 商家与会员直接交易(Busioness To Customer)1.2 B2B批发商城 商家与商家直接交易1.3 B2B2C ...

  4. 无人驾驶8: 粒子滤波定位(优达学城项目)

    优达学城无人车定位的项目实现: 粒子滤波算法流程图 粒子滤波的伪代码: step1:初始化 理论上说,当粒子数量足够多时,能准确地呈现贝叶斯后验分布,如果粒子太少,可能漏掉准确位置,粒子数量太多,会拖 ...

  5. 路飞学城项目之课程搜索关键字接口及支付宝支付功能

    文章目录 1.课程之搜索关键字接口 2.支付宝支付功能 2.1.简单介绍 2.2.aliapy二次封装包 2.2.1.GitHub开源框架 2.2.2.依赖 2.2.3.结构 2.2.4.alipay ...

  6. 路飞学城项目之首页轮播图定时更新、课程页面前端及课程表分析

    文章目录 1.首页轮播图定时更新(使用celery) 2.课程页面前端 3.课程表分析 3.1.课程表相关模型(实战课为例) 3.2.课程表数据录入 3.3.课程分类接口 3.3.课程分类前端展示(对 ...

  7. 2.学城项目 头部底部组件首页轮播图

    1. 下载框架/组件/库 * 1. 下载项目中需要使用的框架个组件. 1. axion 网络请求的第三方框架 (之前测试前后端通信已经安装过了) cnpm install axios -S 2. vu ...

  8. 路飞学城项目之集成支付宝支付功能

    文章目录 1.订单表设计 2.支付接口类:order/views.py 3.支付接口序列化类:model/serializers 4.支付生成页面 5.支付成功的回调页面 6.支付成功的回调接口 1. ...

  9. 路飞学城项目之项目上线

    文章目录 1.购买服务器 2.连接服务器 3.服务器命令 3.1.管理员权限 3.2.配置终端 3.3.更新系统软件包 3.4.安装软件管理包和可能使用的依赖 3.5.检测是否成功:会将git作为依赖 ...

最新文章

  1. raid5 增加一块硬盘_Raid5热备盘上线同时另一块硬盘离线如何恢复数据
  2. 2020-10-29 PYTORCH与Tensorflow速查表
  3. Python日期操作
  4. 网站地图能给网站的优化带来什么好处
  5. 什么叫做类数组对象?
  6. Exynos4412 文件系统制作(三)—— 文件系统移植
  7. CAS单点登陆的两个原理图
  8. java中如何声明外键约束,外键约束不正确 - java-mysql
  9. mysql+odbc+ado_MFC ado+mysql+odbc技术分享
  10. SQL开发技巧(二) 【转】感觉他写的很好
  11. 多线程的那点儿事(之避免死锁)
  12. linux系统oracle监听启动,linux下启动与关闭oracle监听与实例
  13. QT学习资料博客:《Qt 实战一二三》和《Qt 学习之路 2》等
  14. mtk x20 android 开发环境配置
  15. 手机mtkcdc端口如何开启_MTK手机连接电脑说明书
  16. oracle获取字符的长度的函数,oracle取字符串长度的函数length()和hengthb()
  17. Java迷你英雄联盟_Java策略模式:一个关于英雄联盟的例子
  18. ps -- 将图片背景设置成透明的
  19. Spring前一次定时任务没执行完,下次任务是否会执行
  20. LSTM(long short term memory)长短期记忆网络

热门文章

  1. Linux 中的-rwxr-xr-x权限
  2. 大学计算机知识竞赛幽默主持词,关于知识竞赛主持词
  3. 剑与契约如何在电脑上玩 剑与契约模拟器玩法教程
  4. 【关于创业】我要说1
  5. 数据采集工具-免费数据自动采集软件工具
  6. 废旧机械硬盘用于笔记本扩容!!!
  7. C# 坦克游戏大战中学习相关类(Bitmap)
  8. 体重 年龄 性别 身高 预测鞋码_询问父母年龄、身高、体重、鞋号,分别写八组句子。用英语表达。快速求9!...
  9. notepadd++
  10. wince 蓝牙驱动(1) .