在线课程
使用 VitePress 构建现代化的在线课程平台,为学习者提供完整的交互式学习体验。
项目概述
在线教育已成为现代学习的重要方式,一个好的在线课程平台需要提供丰富的学习内容、良好的用户体验和完善的学习跟踪功能。VitePress 凭借其强大的内容管理能力和灵活的扩展性,是构建在线课程平台的理想选择。
核心特性
- 🎥 视频学习 - 高质量的视频播放和字幕支持
- 📚 课程章节 - 结构化的课程内容组织
- ✅ 练习题库 - 丰富的练习题和即时反馈
- 📊 学习进度 - 详细的学习进度跟踪
- 🏆 成就系统 - 学习徽章和证书颁发
- 💬 讨论区 - 学习者交流和答疑
- 📱 移动学习 - 完美的移动端学习体验
- 🔐 用户管理 - 完整的用户注册和权限系统
- 📈 数据分析 - 学习行为和效果分析
- 🌍 多语言 - 国际化课程内容支持
技术架构
核心技术栈
json
{
"frontend": "VitePress + Vue 3",
"video": "Video.js / Plyr",
"auth": "Firebase Auth / Auth0",
"database": "Supabase / Firebase",
"payment": "Stripe / PayPal",
"analytics": "Google Analytics",
"deployment": [
"Vercel",
"Netlify",
"AWS Amplify"
]
}
项目结构
online-course/
├── docs/
│ ├── .vitepress/
│ │ ├── config.ts
│ │ ├── theme/
│ │ │ ├── index.ts
│ │ │ ├── Layout.vue
│ │ │ └── components/
│ │ │ ├── VideoPlayer.vue
│ │ │ ├── QuizComponent.vue
│ │ │ ├── ProgressTracker.vue
│ │ │ └── DiscussionBoard.vue
│ │ └── public/
│ ├── courses/
│ │ ├── web-development/
│ │ ├── data-science/
│ │ ├── mobile-development/
│ │ └── design/
│ ├── dashboard/
│ ├── profile/
│ └── index.md
├── api/
├── scripts/
├── package.json
└── README.md
实现步骤
1. 项目初始化
bash
# 创建项目
npm create vitepress@latest online-course
cd online-course
# 安装依赖
npm install
npm install -D video.js @supabase/supabase-js stripe
# 启动开发服务器
npm run docs:dev
2. 基础配置
typescript
// .vitepress/config.ts
import { defineConfig } from 'vitepress'
export default defineConfig({
title: '在线课程平台',
description: '专业的在线学习平台',
lang: 'zh-CN',
base: '/',
cleanUrls: true,
head: [
['link', { rel: 'icon', href: '/favicon.ico' }],
['meta', { name: 'theme-color', content: '#f59e0b' }],
['meta', { name: 'og:type', content: 'website' }],
['meta', { name: 'og:locale', content: 'zh-CN' }],
['meta', { name: 'og:site_name', content: '在线课程平台' }],
// Video.js CSS
['link', { rel: 'stylesheet', href: 'https://vjs.zencdn.net/8.0.4/video-js.css' }],
// Video.js JavaScript
['script', { src: 'https://vjs.zencdn.net/8.0.4/video.min.js' }],
// 支付系统
['script', { src: 'https://js.stripe.com/v3/' }],
// 分析工具
['script', { async: '', src: 'https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID' }]
],
themeConfig: {
logo: '/logo.svg',
siteTitle: '在线课程',
nav: [
{ text: '首页', link: '/' },
{ text: '课程', link: '/courses/' },
{ text: '我的学习', link: '/dashboard/' },
{ text: '讨论区', link: '/discussions/' },
{
text: '更多',
items: [
{ text: '关于我们', link: '/about' },
{ text: '帮助中心', link: '/help' },
{ text: '联系我们', link: '/contact' }
]
}
],
sidebar: {
'/courses/': [
{
text: 'Web 开发',
collapsed: false,
items: [
{ text: 'HTML/CSS 基础', link: '/courses/web-development/html-css' },
{ text: 'JavaScript 入门', link: '/courses/web-development/javascript' },
{ text: 'Vue.js 实战', link: '/courses/web-development/vue' },
{ text: 'React 开发', link: '/courses/web-development/react' }
]
},
{
text: '数据科学',
collapsed: false,
items: [
{ text: 'Python 基础', link: '/courses/data-science/python' },
{ text: '数据分析', link: '/courses/data-science/analysis' },
{ text: '机器学习', link: '/courses/data-science/ml' },
{ text: '深度学习', link: '/courses/data-science/dl' }
]
},
{
text: '移动开发',
collapsed: false,
items: [
{ text: 'React Native', link: '/courses/mobile-development/react-native' },
{ text: 'Flutter', link: '/courses/mobile-development/flutter' },
{ text: 'iOS 开发', link: '/courses/mobile-development/ios' },
{ text: 'Android 开发', link: '/courses/mobile-development/android' }
]
},
{
text: '设计课程',
collapsed: false,
items: [
{ text: 'UI/UX 设计', link: '/courses/design/ui-ux' },
{ text: '平面设计', link: '/courses/design/graphic' },
{ text: '交互设计', link: '/courses/design/interaction' }
]
}
]
},
socialLinks: [
{ icon: 'github', link: 'https://github.com/your-org' },
{ icon: 'youtube', link: 'https://youtube.com/your-channel' }
],
footer: {
message: '让学习更简单,让成长更快速',
copyright: 'Copyright © 2024 在线课程平台'
},
search: {
provider: 'local',
options: {
locales: {
zh: {
translations: {
button: {
buttonText: '搜索课程',
buttonAriaLabel: '搜索课程'
},
modal: {
noResultsText: '无法找到相关课程',
resetButtonTitle: '清除查询条件',
footer: {
selectText: '选择',
navigateText: '切换'
}
}
}
}
}
}
}
},
markdown: {
theme: {
light: 'github-light',
dark: 'github-dark'
},
config: (md) => {
// 自定义容器
md.use(require('markdown-it-container'), 'lesson')
md.use(require('markdown-it-container'), 'quiz')
md.use(require('markdown-it-container'), 'exercise')
}
}
})
3. 首页设计
vue
<!-- docs/index.md -->
---
layout: home
title: 在线课程平台
titleTemplate: 专业的在线学习平台
hero:
name: 在线课程
text: 开启你的学习之旅
tagline: 专业课程,实战项目,助你快速成长
image:
src: /hero-learning.svg
alt: 在线学习
actions:
- theme: brand
text: 开始学习
link: /courses/
- theme: alt
text: 免费试听
link: /free-trial
features:
- icon: 🎥
title: 高质量视频
details: 专业制作的高清视频课程,支持多种播放速度
link: /courses/
- icon: 📚
title: 结构化学习
details: 系统化的课程设计,从基础到进阶循序渐进
link: /courses/
- icon: ✅
title: 实战练习
details: 丰富的练习题和项目实战,巩固学习效果
link: /courses/
- icon: 📊
title: 学习跟踪
details: 详细的学习进度和成绩统计,了解学习状况
link: /dashboard/
- icon: 🏆
title: 认证证书
details: 完成课程获得专业认证,提升职业竞争力
link: /certificates/
- icon: 💬
title: 社区讨论
details: 与同学和老师互动交流,解决学习疑问
link: /discussions/
---
<script setup>
import { ref, onMounted } from 'vue'
const stats = ref({
totalCourses: 0,
totalStudents: 0,
totalHours: 0,
completionRate: 0
})
const featuredCourses = ref([])
onMounted(async () => {
// 获取平台统计数据
try {
const [statsResponse, coursesResponse] = await Promise.all([
fetch('/api/stats'),
fetch('/api/featured-courses')
])
stats.value = await statsResponse.json()
featuredCourses.value = await coursesResponse.json()
} catch (error) {
console.error('Failed to load data:', error)
}
})
</script>
<div class="platform-stats">
<div class="stats-container">
<div class="stat-item">
<div class="stat-number">{{ stats.totalCourses }}+</div>
<div class="stat-label">精品课程</div>
</div>
<div class="stat-item">
<div class="stat-number">{{ stats.totalStudents }}+</div>
<div class="stat-label">学习者</div>
</div>
<div class="stat-item">
<div class="stat-number">{{ stats.totalHours }}+</div>
<div class="stat-label">课程时长</div>
</div>
<div class="stat-item">
<div class="stat-number">{{ stats.completionRate }}%</div>
<div class="stat-label">完成率</div>
</div>
</div>
</div>
<div class="featured-courses">
<h2>热门课程</h2>
<div class="courses-grid">
<div
v-for="course in featuredCourses"
:key="course.id"
class="course-card"
>
<div class="course-image">
<img :src="course.thumbnail" :alt="course.title">
<div class="course-duration">{{ course.duration }}</div>
</div>
<div class="course-content">
<h3>{{ course.title }}</h3>
<p>{{ course.description }}</p>
<div class="course-meta">
<span class="instructor">{{ course.instructor }}</span>
<span class="rating">⭐ {{ course.rating }}</span>
</div>
<div class="course-price">
<span v-if="course.originalPrice" class="original-price">¥{{ course.originalPrice }}</span>
<span class="current-price">¥{{ course.price }}</span>
</div>
</div>
</div>
</div>
</div>
<style>
:root {
--vp-home-hero-name-color: transparent;
--vp-home-hero-name-background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
--vp-home-hero-image-background-image: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
--vp-home-hero-image-filter: blur(44px);
}
.platform-stats {
margin: 48px 0;
padding: 40px 0;
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
border-radius: 16px;
color: white;
}
.stats-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 32px;
max-width: 800px;
margin: 0 auto;
padding: 0 24px;
}
.stat-item {
text-align: center;
}
.stat-number {
font-size: 36px;
font-weight: 700;
margin-bottom: 8px;
}
.stat-label {
font-size: 16px;
opacity: 0.9;
}
.featured-courses {
margin: 64px 0;
}
.featured-courses h2 {
text-align: center;
margin-bottom: 32px;
font-size: 32px;
color: var(--vp-c-text-1);
}
.courses-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 24px;
max-width: 1200px;
margin: 0 auto;
}
.course-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-border);
border-radius: 12px;
overflow: hidden;
transition: all 0.2s ease;
cursor: pointer;
}
.course-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 32px rgba(245, 158, 11, 0.15);
border-color: var(--vp-c-brand);
}
.course-image {
position: relative;
aspect-ratio: 16/9;
overflow: hidden;
}
.course-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.course-duration {
position: absolute;
bottom: 8px;
right: 8px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
}
.course-content {
padding: 20px;
}
.course-content h3 {
margin: 0 0 8px 0;
font-size: 18px;
color: var(--vp-c-text-1);
}
.course-content p {
margin: 0 0 12px 0;
color: var(--vp-c-text-2);
font-size: 14px;
line-height: 1.5;
}
.course-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
font-size: 12px;
}
.instructor {
color: var(--vp-c-text-2);
}
.rating {
color: var(--vp-c-brand);
font-weight: 500;
}
.course-price {
display: flex;
align-items: center;
gap: 8px;
}
.original-price {
text-decoration: line-through;
color: var(--vp-c-text-3);
font-size: 14px;
}
.current-price {
color: var(--vp-c-brand);
font-size: 18px;
font-weight: 600;
}
.VPFeature {
transition: all 0.2s ease;
}
.VPFeature:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(245, 158, 11, 0.15);
}
@media (max-width: 768px) {
.stats-container {
grid-template-columns: repeat(2, 1fr);
gap: 20px;
}
.stat-number {
font-size: 28px;
}
.courses-grid {
grid-template-columns: 1fr;
}
}
</style>
4. 视频播放器组件
vue
<!-- .vitepress/theme/components/VideoPlayer.vue -->
<template>
<div class="video-player-container">
<div class="video-header">
<h3>{{ lesson.title }}</h3>
<div class="video-controls">
<button @click="togglePlaybackSpeed" class="speed-btn">
{{ playbackSpeed }}x
</button>
<button @click="toggleFullscreen" class="fullscreen-btn">
<FullscreenIcon />
</button>
</div>
</div>
<div class="video-wrapper">
<video
ref="videoRef"
class="video-js vjs-default-skin"
controls
preload="auto"
:poster="lesson.poster"
data-setup="{}"
>
<source :src="lesson.videoUrl" type="video/mp4">
<track
v-if="lesson.subtitles"
kind="subtitles"
:src="lesson.subtitles"
srclang="zh"
label="中文"
default
>
<p class="vjs-no-js">
要观看此视频,请启用 JavaScript,并考虑升级到
<a href="https://videojs.com/html5-video-support/" target="_blank">
支持HTML5视频的网络浏览器
</a>。
</p>
</video>
<div v-if="showNotes" class="video-notes">
<div class="notes-header">
<h4>课程笔记</h4>
<button @click="showNotes = false" class="close-btn">×</button>
</div>
<div class="notes-content">
<textarea
v-model="userNotes"
placeholder="在这里记录你的学习笔记..."
@input="saveNotes"
></textarea>
</div>
</div>
</div>
<div class="video-footer">
<div class="progress-info">
<span>进度: {{ Math.round(watchProgress) }}%</span>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: watchProgress + '%' }"></div>
</div>
</div>
<div class="video-actions">
<button @click="showNotes = !showNotes" class="notes-btn">
<NotesIcon />
笔记
</button>
<button @click="markAsCompleted" class="complete-btn" :disabled="!canMarkComplete">
<CheckIcon />
标记完成
</button>
</div>
</div>
<div class="lesson-navigation">
<button
v-if="previousLesson"
@click="navigateToLesson(previousLesson)"
class="nav-btn prev-btn"
>
← 上一课: {{ previousLesson.title }}
</button>
<button
v-if="nextLesson"
@click="navigateToLesson(nextLesson)"
class="nav-btn next-btn"
>
下一课: {{ nextLesson.title }} →
</button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { useRouter } from 'vitepress'
import videojs from 'video.js'
import FullscreenIcon from './FullscreenIcon.vue'
import NotesIcon from './NotesIcon.vue'
import CheckIcon from './CheckIcon.vue'
const props = defineProps({
lesson: {
type: Object,
required: true
},
previousLesson: Object,
nextLesson: Object
})
const router = useRouter()
const videoRef = ref()
const showNotes = ref(false)
const userNotes = ref('')
const watchProgress = ref(0)
const playbackSpeed = ref(1)
const canMarkComplete = ref(false)
let player = null
let progressInterval = null
onMounted(() => {
initializePlayer()
loadUserNotes()
loadWatchProgress()
})
onUnmounted(() => {
if (player) {
player.dispose()
}
if (progressInterval) {
clearInterval(progressInterval)
}
})
watch(() => props.lesson, (newLesson) => {
if (player && newLesson) {
player.src({ src: newLesson.videoUrl, type: 'video/mp4' })
player.poster(newLesson.poster)
loadUserNotes()
loadWatchProgress()
}
})
function initializePlayer() {
player = videojs(videoRef.value, {
fluid: true,
responsive: true,
playbackRates: [0.5, 1, 1.25, 1.5, 2],
plugins: {
hotkeys: {
volumeStep: 0.1,
seekStep: 5,
enableModifiersForNumbers: false
}
}
})
player.ready(() => {
// 监听播放进度
progressInterval = setInterval(() => {
if (player && !player.paused()) {
const currentTime = player.currentTime()
const duration = player.duration()
if (duration > 0) {
const progress = (currentTime / duration) * 100
watchProgress.value = progress
// 观看超过80%可以标记完成
canMarkComplete.value = progress >= 80
// 保存观看进度
saveWatchProgress(progress)
}
}
}, 1000)
})
// 监听播放速度变化
player.on('ratechange', () => {
playbackSpeed.value = player.playbackRate()
})
// 监听全屏变化
player.on('fullscreenchange', () => {
// 处理全屏状态变化
})
}
function togglePlaybackSpeed() {
const speeds = [0.5, 1, 1.25, 1.5, 2]
const currentIndex = speeds.indexOf(playbackSpeed.value)
const nextIndex = (currentIndex + 1) % speeds.length
const newSpeed = speeds[nextIndex]
player.playbackRate(newSpeed)
playbackSpeed.value = newSpeed
}
function toggleFullscreen() {
if (player.isFullscreen()) {
player.exitFullscreen()
} else {
player.requestFullscreen()
}
}
async function loadUserNotes() {
try {
const response = await fetch(`/api/lessons/${props.lesson.id}/notes`)
const data = await response.json()
userNotes.value = data.notes || ''
} catch (error) {
console.error('Failed to load notes:', error)
}
}
async function saveNotes() {
try {
await fetch(`/api/lessons/${props.lesson.id}/notes`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
notes: userNotes.value
})
})
} catch (error) {
console.error('Failed to save notes:', error)
}
}
async function loadWatchProgress() {
try {
const response = await fetch(`/api/lessons/${props.lesson.id}/progress`)
const data = await response.json()
watchProgress.value = data.progress || 0
canMarkComplete.value = watchProgress.value >= 80
} catch (error) {
console.error('Failed to load progress:', error)
}
}
async function saveWatchProgress(progress) {
try {
await fetch(`/api/lessons/${props.lesson.id}/progress`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
progress: progress,
timestamp: new Date().toISOString()
})
})
} catch (error) {
console.error('Failed to save progress:', error)
}
}
async function markAsCompleted() {
if (!canMarkComplete.value) return
try {
await fetch(`/api/lessons/${props.lesson.id}/complete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
completedAt: new Date().toISOString()
})
})
// 显示完成提示
alert('恭喜!你已完成本课学习')
// 记录学习事件
if (typeof gtag !== 'undefined') {
gtag('event', 'lesson_completed', {
event_category: 'learning',
event_label: props.lesson.id,
value: 1
})
}
} catch (error) {
console.error('Failed to mark as completed:', error)
}
}
function navigateToLesson(lesson) {
router.go(lesson.url)
}
</script>
<style scoped>
.video-player-container {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-border);
border-radius: 12px;
overflow: hidden;
margin: 24px 0;
}
.video-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-border);
}
.video-header h3 {
margin: 0;
font-size: 18px;
color: var(--vp-c-text-1);
}
.video-controls {
display: flex;
gap: 8px;
}
.speed-btn,
.fullscreen-btn {
padding: 6px 12px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-border);
border-radius: 6px;
color: var(--vp-c-text-2);
cursor: pointer;
transition: all 0.2s;
font-size: 12px;
}
.speed-btn:hover,
.fullscreen-btn:hover {
background: var(--vp-c-bg-mute);
color: var(--vp-c-text-1);
}
.video-wrapper {
position: relative;
}
.video-js {
width: 100%;
height: auto;
}
.video-notes {
position: absolute;
top: 20px;
right: 20px;
width: 300px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-border);
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
z-index: 10;
}
.notes-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
backgroun
# Online Course
本文档正在建设中,敬请期待。
## 概述
这里将提供关于 Online Course 的详细信息和指导。
## 主要内容
- 基础概念介绍
- 使用方法说明
- 最佳实践建议
- 常见问题解答
## 相关资源
- [VitePress 官方文档](https://vitepress.dev/)
- [Vue.js 官方文档](https://vuejs.org/)
- [更多教程](../tutorials/index)
---
*本文档将持续更新,如有问题请通过 [GitHub Issues](https://github.com/shingle666) 反馈。*