| |
| 任务.delay() |
| 任务.apply_async(args=[],eta=时间对象) |
| celery.py 的app的配置信息 |
| app.conf.beat_schedule |
| 启动beat和worker |
| |
| |
| |
| |
| |
| |
| |
| -写一个类,继承这个类,查询所有就有缓存,没继承就没有缓存 |
| |
| -common_view.py |
| class CacheListModelMixin(ListModelMixin): |
| cache_key = None |
| def list(self, request, *args, **kwargs): |
| data = cache.get(self.cache_key) |
| if data: |
| return APIResponse(data=data) |
| |
| res = super(CacheListModelMixin, self).list(request, *args, **kwargs) |
| cache.set(self.cache_key, res.data) |
| return APIResponse(data=res.data) |
| |
| |
| |
| -因为加了缓存,数据库编号,缓存不会编号,导致数据不一致 |
| -解决问题: |
| 1 定时更新 |
| 2 修改数据库,更新缓存 |
| 3 修改数据,删除缓存 |
| |
| -home_task.py 新建一个任务(函数),update_banner |
| -celery.py 给app配置一个定时任务 |
| -启动worker,启动beat |
| |
| |
| -课程分类表 |
| -课程表(实战课表) |
| -章节表 |
| -课时表 |
| -老师表 |
| |
| |
1 课程表数据录入
| -- 老师表 |
| 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, '2022-07-14 13:44:19.661327', '2022-07-14 13:46:54.246271', 'Alex', 1, '老男孩Python教学总监', '金角大王', 'teacher/alex_icon.png', '老男孩教育CTO & CO-FOUNDER 国内知名PYTHON语言推广者 51CTO学院2016\2017年度最受学员喜爱10大讲师之一 多款开源软件作者 曾任职公安部、飞信、中金公司、NOKIA中国研究院、华尔街英语、ADVENT、汽车之家等公司'); |
| |
| 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, '2022-07-14 13:45:25.092902', '2022-07-14 13:45:25.092936', 'Mjj', 0, '前美团前端项目组架构师', NULL, 'teacher/mjj_icon.png', '是马JJ老师, 一个集美貌与才华于一身的男人,搞过几年IOS,又转了前端开发几年,曾就职于美团网任高级前端开发,后来因为不同意王兴(美团老板)的战略布局而出家做老师去了,有丰富的教学经验,开起车来也毫不含糊。一直专注在前端的前沿技术领域。同时,爱好抽烟、喝酒、烫头(锡纸烫)。 我的最爱是前端,因为前端妹子多。'); |
| |
| 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, '2022-07-14 13:46:21.997846', '2022-07-14 13:46:21.997880', 'Lyy', 0, '老男孩Linux学科带头人', NULL, 'teacher/lyy_icon.png', 'Linux运维技术专家,老男孩Linux金牌讲师,讲课风趣幽默、深入浅出、声音洪亮到爆炸'); |
| -- 分类表 |
| INSERT INTO luffy_course_category(id, orders, is_show, is_delete, created_time, updated_time, name) VALUES (1, 1, 1, 0, '2022-07-14 13:40:58.690413', '2022-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, '2022-07-14 13:41:08.249735', '2022-07-14 13:41:08.249817', 'Linux'); |
| -- 课程表 |
| 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, '2022-07-14 13:54:33.095201', '2022-07-14 13:54:33.095238', 'Python开发21天入门', 'courses/alex_python.png', 0, 'Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土', 0, '2022-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, '2022-07-14 13:56:05.051103', '2022-07-14 13:56:05.051142', 'Python项目实战', 'courses/mjj_python.png', 0, '', 1, '2022-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, '2022-07-14 13:57:21.190053', '2022-07-14 13:57:21.190095', 'Linux系统基础5周入门精讲', 'courses/lyy_linux.png', 0, '', 0, '2022-07-14', 25, '', 0, 219, 100, 100, 39.00, 2, 3); |
| -- 章节表 |
| 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, '2022-07-14 13:58:34.867005', '2022-07-14 14:00:58.276541', 1, '计算机原理', '', '2022-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, '2022-07-14 13:58:48.051543', '2022-07-14 14:01:22.024206', 2, '环境搭建', '', '2022-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, '2022-07-14 13:59:09.878183', '2022-07-14 14:01:40.048608', 1, '项目创建', '', '2022-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, '2022-07-14 13:59:37.448626', '2022-07-14 14:01:58.709652', 1, 'Linux环境创建', '', '2022-07-14', 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 (1, 1, 0, '2022-07-14 14:02:33.779098', '2022-07-14 14:02:33.779135', '计算机原理上', 1, 2, NULL, NULL, '2022-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, '2022-07-14 14:02:56.657134', '2022-07-14 14:02:56.657173', '计算机原理下', 2, 2, NULL, NULL, '2022-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, '2022-07-14 14:03:20.493324', '2022-07-14 14:03:52.329394', '环境搭建上', 1, 2, NULL, NULL, '2022-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, '2022-07-14 14:03:36.472742', '2022-07-14 14:03:36.472779', '环境搭建下', 2, 2, NULL, NULL, '2022-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, '2022-07-14 14:04:19.338153', '2022-07-14 14:04:19.338192', 'web项目的创建', 1, 2, NULL, NULL, '2022-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, '2022-07-14 14:04:52.895855', '2022-07-14 14:04:52.895890', 'Linux的环境搭建', 1, 2, NULL, NULL, '2022-07-14 14:04:52.895942', 1, 4); |
2 课程分类接口
2.1 路由
| router.register('category', views.CourseCategoryView, 'category') |
2.2 序列化类
| class CourseCategorySerializer(serializers.ModelSerializer): |
| class Meta: |
| model = CourseCategory |
| fields = ['id', 'name'] |
| |
2.3 视图类
| class CourseCategoryView(GenericViewSet, CommonListModelMixin): |
| queryset = CourseCategory.objects.filter(is_delete=False, is_show=True).order_by('orders') |
| serializer_class = CourseCategorySerializer |
3 所有课程接口(过滤,排序)
| |
| |
| |
| |
| -老师的数据 |
| -章节和课时:如果课时数大于4,就返回4条,如果小于4,有多少就返回多少 |
3.1 表模型
| class Course(BaseModel): |
| ... |
| |
| def __str__(self): |
| return "%s" % self.name |
| |
| def course_type_name(self): |
| return self.get_course_type_display() |
| |
| def level_name(self): |
| return self.get_level_display() |
| |
| def status_name(self): |
| return self.get_status_display() |
| |
| def section_list(self): |
| ''' |
| # 逻辑: |
| #1 先根据课程,拿到所有章节 |
| #2 循环所有章节,拿出每个章节下的课时 |
| #3 拼到一个列表中 |
| # 4 判断一下列表长度是否大于等于四,直接 return |
| ''' |
| |
| |
| l = [] |
| course_chapter_list = self.coursechapters.all() |
| for course_chapter in course_chapter_list: |
| |
| course_section_list = course_chapter.coursesections.all() |
| for course_section in course_section_list: |
| l.append({ |
| 'name': course_section.name, |
| 'section_link': course_section.section_link, |
| 'duration': course_section.duration, |
| 'free_trail': course_section.free_trail, |
| }) |
| if len(l) >= 4: |
| return l |
| |
| return l |
3.2 序列化类
| |
| class TeacherSerializer(serializers.ModelSerializer): |
| class Meta: |
| model = Teacher |
| fields = [ |
| 'id', 'name', 'role_name', 'title', 'signature', 'image', 'brief' |
| ] |
| |
| |
| class CourseSerializer(serializers.ModelSerializer): |
| teacher = TeacherSerializer() |
| |
| class Meta: |
| model = Course |
| fields = [ |
| 'id', |
| 'name', |
| 'course_img', |
| 'brief', |
| 'attachment_path', |
| 'pub_sections', |
| 'price', |
| 'students', |
| 'period', |
| 'sections', |
| |
| 'course_type_name', |
| 'level_name', |
| 'status_name', |
| |
| 'teacher', |
| 'section_list', |
| ] |
3.3 视图类
| class CourseView(GenericViewSet, CommonListModelMixin): |
| queryset = Course.objects.filter(is_delete=False, is_show=True).order_by('orders') |
| serializer_class = CourseSerializer |
| |
| pagination_class = PageNumberPagination |
| |
| filter_backends = [OrderingFilter, DjangoFilterBackend] |
| ordering_fields = ['price', 'students'] |
| |
| |
| filterset_fields = ['course_category'] |
3.4 路由
| |
| router.register('course', views.CourseView, 'course') |
| |
4 课程详情接口(没有章节和课时的内容)
| class CourseView(GenericViewSet, CommonListModelMixin, RetrieveModelMixin): |
| ... |
| def retrieve(self, request, *args, **kwargs): |
| res = super().retrieve(request, *args, **kwargs) |
| return APIResponse(data=res.data) |
| |
5 所有章节接口(按课程过滤)
5.1 视图类
| class CourseChapterView(GenericViewSet, CommonListModelMixin): |
| queryset = CourseChapter.objects.filter(is_delete=False, is_show=True).order_by('orders') |
| serializer_class = CourseChapterSerializer |
| filter_backends = [DjangoFilterBackend] |
| filterset_fields = ['course'] |
5.2 序列化类
| |
| class CourseSectionSerializer(serializers.ModelSerializer): |
| class Meta: |
| model = CourseSection |
| |
| fields = ('id', 'name', 'orders', 'section_link', 'duration', 'free_trail') |
| |
| |
| |
| class CourseChapterSerializer(serializers.ModelSerializer): |
| |
| coursesections = CourseSectionSerializer(many=True) |
| |
| class Meta: |
| model = CourseChapter |
| |
| fields = ('id', 'name', 'chapter', 'summary', 'coursesections') |
5.3 路由
| |
| router.register('coursechapter', views.CourseChapterView, 'coursechapter') |
6 课程列表前端
| <template> |
| <div class="course"> |
| <Header></Header> |
| <div class="main"> |
| <!-- 筛选条件 --> |
| <div class="condition"> |
| <ul class="cate-list"> |
| <li class="title">课程分类:</li> |
| <li :class="filter.course_category==0?'this':''" @click="filter.course_category=0">全部</li> |
| <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">筛 选:</li> |
| <li class="default" :class="(filter.ordering=='id' || filter.ordering=='-id')?'this':''" |
| @click="filter.ordering='-id'">默认 |
| </li> |
| <li class="hot" :class="(filter.ordering=='students' || filter.ordering=='-students')?'this':''" |
| @click="filter.ordering=(filter.ordering=='-students'?'students':'-students')">人气 |
| </li> |
| <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="teather-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"> |
| <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/Header" |
| import Footer from "@/components/Footer" |
| |
| export default { |
| name: "Course", |
| data() { |
| return { |
| category_list: [], // 课程分类列表 |
| course_list: [], // 课程列表 |
| course_total: 0, // 当前课程的总数量 |
| filter: { |
| course_category: 0, // 当前用户选择的课程分类,刚进入页面默认为全部,值为0 |
| ordering: "-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/category/`).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; |
| } |
| |
| |
| // 获取课程列表信息 |
| this.$axios.get(`${this.$settings.BASE_URL}/course/course/`, { |
| params: filters |
| }).then(response => { |
| // console.log(response.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: 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 .teather-info { |
| font-size: 14px; |
| color: #9b9b9b; |
| margin-bottom: 14px; |
| padding-bottom: 14px; |
| border-bottom: 1px solid #333; |
| border-bottom-color: rgba(51, 51, 51, .05); |
| } |
| |
| .course-item .course-info .teather-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> |
| |
7 课程详情前端
| |
| |
| 1 安装 |
| cnpm install vue-core-video-player -S |
| |
| 2 在main.js配置 |
| import VueCoreVideoPlayer from 'vue-core-video-player' |
| Vue.use(VueCoreVideoPlayer, { |
| lang: 'zh-CN' |
| }) |
| |
| 3 在使用的组件中,使用即可 |
| <vue-core-video-player |
| src="https://video.pearvideo.com/mp4/third/20230307/cont-1779481-10023871-222053-hd.mp4" |
| @play="onPlayerPlay" |
| @pause="onPlayerPause" |
| title="课程名字" |
| controls='auto' |
| > |
| </vue-core-video-player> |
| |
| |
| onPlayerPlay() { |
| // 当视频播放时,执行的方法 |
| console.log('视频开始播放') |
| }, |
| onPlayerPause() { |
| // 当视频暂停播放时,执行的方法 |
| console.log('视频暂停,可以打开广告了') |
| }, |
| |
| |
| 4 更详细:https://www.cnblogs.com/liuqingzheng/p/16204851.html |
| |
| |
| |
| |
| |
| |
| |
| 1 课程详情接口,根据id查 |
| 2 课程章节接口 (单独写的课程章节接口,但它是可以放在课程详情接口中的) |
| 3 查询老师接口(不需要,课程详情接口中有老师所有信息了) |
| <template> |
| <div class="detail"> |
| <Header/> |
| <div class="main"> |
| <div class="course-info"> |
| <div class="wrap-left"> |
| <vue-core-video-player |
| src="https://video.pearvideo.com/mp4/third/20230307/cont-1779481-10023871-222053-hd.mp4" |
| @play="onPlayerPlay" |
| @pause="onPlayerPause" |
| title="课程名字" |
| controls='auto' |
| > |
| </vue-core-video-player> |
| </div> |
| <div class="wrap-right"> |
| <h3 class="course-name">{{ course_info.name }}</h3> |
| <p class="data"> |
| {{ course_info.students }}人在学 课程总时长:{{ course_info.sections }}课时/{{ course_info.pub_sections }}小时 难度:{{ 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"> |
| <li :class="tabIndex==1?'active':''" @click="tabIndex=1">详情介绍</li> |
| <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/Header" |
| import Footer from "@/components/Footer" |
| |
| |
| export default { |
| name: "Detail", |
| data() { |
| return { |
| tabIndex: 2, // 当前选项卡显示的下标 |
| course_id: 0, // 当前课程信息的ID |
| course_info: { |
| teacher: {}, |
| }, // 课程信息 |
| course_chapters: [], // 课程的章节课时列表 |
| } |
| }, |
| created() { |
| this.get_course_id(); |
| this.get_course_data(); |
| this.get_chapter(); |
| }, |
| methods: { |
| onPlayerPlay() { |
| // 当视频播放时,执行的方法 |
| console.log('视频开始播放') |
| }, |
| onPlayerPause() { |
| // 当视频暂停播放时,执行的方法 |
| console.log('视频暂停,可以打开广告了') |
| }, |
| get_course_id() { |
| // 获取地址栏上面的课程ID |
| this.course_id = this.$route.params.pk || this.$route.query.pk; |
| 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, |
| } |
| } |
| </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> |
| |
补充
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现