Backend Data Modeling and API Endpoints
To support course browsing and detailed viewing, the backend defines several interrelated models:
- Course: Contains fields like
name,level,price,sales,hours,total_jie(total sections),video_url(intro video), andquestion(FAQ). - Chapter: Linked to
Coursevia foreign key; includesname,order,summary, andduration_seconds. - Section: Belongs to both
CourseandChapter; storesname,order,video_url,duration_seconds, andis_free_trial. - Instructor: Holds
name,avatar,introduce,role, and a reverse relation to courses taught. - Comment and Reply: Support user feedback with timestamps, ratings, and nested replies.
Key API views include:
# Retrieve full course details including instructor, chapters, and metadata
class CourseDetailView(APIView):
def get(self, request, pk):
cache_key = f"course_detail_{pk}"
cached = cache.get(cache_key)
if cached:
return Response({"code": 200, "data": cached})
course = get_object_or_404(CourseModel, id=pk)
serializer = CourseDetailSerializer(course)
data = serializer.data
cache.set(cache_key, data, timeout=600) # 10-minute TTL
return Response({"code": 200, "data": data})
# Fetch all chapters for a given course
class ChapterListView(APIView):
def get(self, request, course_id):
chapters = ChapterModel.objects.filter(course_id=course_id).prefetch_related(
'sections'
).order_by('order')
serializer = ChapterListSerializer(chapters, many=True)
return Response({"code": 200, "data": serializer.data})
# Fetch comments and associated replies for a course
class CommentThreadView(APIView):
def get(self, request, course_id):
comments = CommentModel.objects.filter(
course_id=course_id,
is_approved=True
).select_related('user').prefetch_related('replies__user')
serializer = CommentThreadSerializer(comments, many=True)
return Response({"code": 200, "data": serializer.data})
URL routing:
urlpatterns = [
path('courses/<int:pk>/', CourseDetailView.as_view()),
path('courses/<int:course_id>/chapters/', ChapterListView.as_view()),
path('courses/<int:course_id>/comments/', CommentThreadView.as_view()),
]
Frontend Course Detail Implementation
The Vue 3 component Info.vue renders course information using reactive state and asynchronous data loading. It supports tabbed navigation between Overview, Chapters, Reveiws, and FAQ.
Video Player Integration
A responsive video player is embedded using vue-aliplayer-v2. The player dynamical loads video sources from the course's video_url field and displays a cover image:
<template>
<AliPlayerV3
ref="playerRef"
class="w-full rounded-lg aspect-video md:aspect-[16/9]"
:source="courseData.video_url"
:cover="courseData.cover_image"
:options="{
autoplay: false,
controls: true,
width: '100%',
height: '100%',
useH5Prism: true
}"
@play="handlePlay"
@pause="handlePause"
/>
</template>
Alternative native <video> implementation (fallback or lightweight usage):
<video class="w-full rounded-lg aspect-video" controls preload="metadata">
<source :src="courseData.video_url" type="video/mp4" />
Your browser does not support the video tag.
</video>
Tabbed Content Rendering
Tab switching uses a simple numeric index (tabIndex) bound to conditional rendering:
<div class="tab-content">
<div v-if="tabIndex === 1" v-html="courseData.description"></div>
<div v-if="tabIndex === 2">
<h3 class="text-xl font-bold mb-4">Course Curriculum</h3>
<p class="mb-3">{{ chapters.length }} chapters • {{ courseData.hours }} hours</p>
<div v-for="chapter in chapters" :key="chapter.id" class="mb-6">
<h4 class="font-medium flex items-center gap-2">
<span class="text-blue-600">Ch. {{ chapter.order }}</span>
{{ chapter.name }}
</h4>
<p v-if="chapter.summary" class="text-gray-600 mt-1" v-html="chapter.summary"></p>
<ul class="mt-3 space-y-2">
<li v-for="section in chapter.sections" :key="section.id" class="flex justify-between items-center p-2 rounded hover:bg-gray-50">
<span class="flex items-center gap-2">
<span>{{ chapter.order }}.{{ section.order }}</span>
{{ section.name }}
<span v-if="section.is_free_trial" class="text-xs bg-green-100 text-green-800 px-2 py-0.5 rounded">Free</span>
</span>
<span class="text-sm text-gray-500">{{ formatDuration(section.duration_seconds) }}</span>
</li>
</ul>
</div>
</div>
<div v-if="tabIndex === 3">
<h3 class="text-xl font-bold mb-4">Student Reviews</h3>
<div v-for="comment in comments" :key="comment.id" class="border-b pb-4 mb-4 last:border-0 last:mb-0">
<div class="flex items-center gap-3 mb-2">
<img :src="comment.user.avatar" class="w-10 h-10 rounded-full" />
<div>
<p class="font-medium">{{ comment.user.username }}</p>
<p class="text-sm text-gray-500">{{ comment.created_at | formatDate }}</p>
</div>
</div>
<p v-html="comment.content"></p>
<div v-if="comment.replies.length" class="mt-3 pl-6 border-l-2 border-gray-200">
<div v-for="reply in comment.replies" :key="reply.id" class="mb-2">
<strong>{{ reply.user.username }}:</strong> {{ reply.content }}
</div>
</div>
</div>
</div>
<div v-if="tabIndex === 4" v-html="courseData.question"></div>
</div>
Relevant composition setup:
// src/composables/useCourse.js
import { ref, onMounted } from 'vue';
import http from '@/utils/http';
export function useCourse(id) {
const courseData = ref({});
const chapters = ref([]);
const comments = ref([]);
const tabIndex = ref(1);
const loadCourse = async () => {
const res = await http.get(`/api/courses/${id}/`);
courseData.value = res.data.data;
};
const loadChapters = async () => {
const res = await http.get(`/api/courses/${id}/chapters/`);
chapters.value = res.data.data;
};
const loadComments = async () => {
const res = await http.get(`/api/courses/${id}/comments/`);
comments.value = res.data.data;
};
onMounted(async () => {
await Promise.all([
loadCourse(),
loadChapters(),
loadComments()
]);
});
return {
courseData,
chapters,
comments,
tabIndex
};
}
Supporting Utilities
A helper function formats seconds into HH:MM:SS:
// src/utils/time.js
export function formatDuration(seconds) {
const hrs = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
return `${hrs}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
Caching is implemented via Django’s built-in cache framework, reducing database load for frequently accessed course pages.