hookehuyr

feat(课程详情): 添加音频播放器支持并优化课程类型显示

在课程详情页中增加对音频课程的支持,添加音频播放器组件。同时,优化课程类型显示逻辑,将课程类型统一为英文标识(video/audio),并在前端显示时转换为中文。此外,调整了音频播放器的样式和布局,以提升用户体验。
......@@ -11,7 +11,7 @@
<van-image :src="course.thumbnail" width="120" height="100%" fit="cover" class="item-cover">
<div class="absolute bg-white/80 px-2 py-0.5 text-xs rounded"
style="right: 0.25rem; bottom: 0.5rem">
{{ course.type }}
{{ course.type === 'video' ? '视频' : '音频' }}
</div>
</van-image>
</div>
......
......@@ -7,56 +7,56 @@
-->
<template>
<!-- 音频播放器主容器 -->
<div class="audio-player bg-white rounded-lg shadow-xl overflow-hidden max-w-md mx-auto p-4">
<div class="audio-player bg-white rounded-lg shadow-xl overflow-hidden max-w-md mx-auto p-2">
<!-- 封面与歌曲信息 -->
<div class="flex flex-col items-center mb-6">
<div class="flex flex-col items-center mb-4">
<!-- 歌曲封面 -->
<div class="w-48 h-48 rounded-lg overflow-hidden mb-4">
<div class="w-24 h-24 rounded-lg overflow-hidden mb-2">
<img :src="currentSong.cover" alt="封面" class="w-full h-full object-cover">
</div>
<!-- 歌曲信息 -->
<div class="text-center">
<h3 class="text-lg font-medium">{{ currentSong.title }}</h3>
<p class="text-sm text-gray-500">{{ currentSong.artist }}</p>
<h3 class="text-base font-medium">{{ currentSong.title }}</h3>
<p class="text-xs text-gray-500">{{ currentSong.artist }}</p>
</div>
</div>
<!-- 进度条与时间:显示当前播放时间、总时长和可拖动的进度条 -->
<div class="mt-4 mb-6">
<div class="flex items-center justify-between text-xs text-gray-400 mb-2">
<span>{{ formatTime(currentTime) }}</span>
<span>{{ formatTime(duration) }}</span>
</div>
<div class="progress-bar relative h-1 bg-gray-200 rounded-full">
<input
type="range"
:value="progress"
@input="handleProgressChange"
@change="seekTo"
class="w-full absolute inset-0 appearance-none bg-transparent cursor-pointer"
>
<div
:style="{ width: `${progress}%` }"
class="progress-track absolute left-0 top-0 h-full rounded-full bg-gradient-to-r from-blue-500 to-purple-500 transition-all"
></div>
<div class="flex items-center space-x-4">
<span class="text-xs text-gray-400 w-12 text-right">{{ formatTime(currentTime) }}</span>
<div class="progress-bar relative h-1 bg-gray-200 rounded-full flex-1">
<input
type="range"
:value="progress"
@input="handleProgressChange"
@change="seekTo"
class="w-full absolute inset-0 appearance-none bg-transparent cursor-pointer"
>
<div
:style="{ width: `${progress}%` }"
class="progress-track absolute left-0 top-0 h-full rounded-full bg-gradient-to-r from-blue-500 to-purple-500 transition-all"
></div>
</div>
<span class="text-xs text-gray-400 w-12">{{ formatTime(duration) }}</span>
</div>
</div>
<!-- 播放控制按钮组:上一首、播放/暂停、下一首 -->
<div class="flex items-center space-x-16 mt-6" style="justify-content: space-evenly;">
<div class="flex items-center space-x-12 mt-4" style="justify-content: space-evenly;">
<button
@click="prevSong"
class="w-14 h-14 flex items-center justify-center rounded-full bg-gray-100 hover:bg-gray-200 transition-colors"
class="w-10 h-10 flex items-center justify-center rounded-full bg-gray-100 hover:bg-gray-200 transition-colors"
>
<font-awesome-icon icon="backward-step" class="text-2xl text-gray-600" />
<font-awesome-icon icon="backward-step" class="text-xl text-gray-600" />
</button>
<button
@click="togglePlay"
:class="{'paused': !isPlaying, 'opacity-50 cursor-not-allowed': isLoading}"
:disabled="isLoading"
class="w-14 h-14 flex items-center justify-center rounded-full bg-blue-500 hover:bg-blue-600 transition-colors shadow-lg"
class="w-12 h-12 flex items-center justify-center rounded-full bg-blue-500 hover:bg-blue-600 transition-colors shadow-lg"
>
<font-awesome-icon
:icon="['fas' , isPlaying ? 'pause' : 'play']"
......@@ -66,9 +66,9 @@
</button>
<button
@click="nextSong"
class="w-14 h-14 flex items-center justify-center rounded-full bg-gray-100 hover:bg-gray-200 transition-colors"
class="w-10 h-10 flex items-center justify-center rounded-full bg-gray-100 hover:bg-gray-200 transition-colors"
>
<font-awesome-icon icon="forward-step" class="text-2xl text-gray-600" />
<font-awesome-icon icon="forward-step" class="text-xl text-gray-600" />
</button>
</div>
......@@ -112,7 +112,7 @@
<div class="text-sm text-gray-500">{{ song.artist }}</div>
</div>
<font-awesome-icon
v-if="index === currentIndex"
v-if="index === currentIndex && isPlaying"
icon="volume-up"
class="text-blue-500 ml-2"
/>
......@@ -281,8 +281,13 @@ watch(() => props.songs, () => {
// 组件卸载时清理事件监听
onUnmounted(() => {
audio.value?.removeEventListener('timeupdate', updateProgress)
audio.value?.removeEventListener('ended', handleEnded)
if (audio.value) {
audio.value.pause();
audio.value.removeEventListener('timeupdate', updateProgress);
audio.value.removeEventListener('ended', handleEnded);
audio.value = null;
}
isPlaying.value = false;
})
// 播放列表相关方法
......
......@@ -8,7 +8,7 @@
<!-- 固定区域:视频播放和标签页 -->
<div class="fixed top-0 left-0 right-0 z-10 top-wrapper">
<!-- 视频播放区域 -->
<div class="w-full bg-black relative">
<div v-if="course.type === 'video'" class="w-full bg-black relative">
<!-- 视频封面和播放按钮 -->
<div v-if="!isPlaying" class="relative w-full" style="aspect-ratio: 16/9;">
<img :src="course.cover" :alt="course.title" class="w-full h-full object-cover" />
......@@ -25,6 +25,10 @@
<VideoPlayer v-show="isPlaying" ref="videoPlayerRef" :video-url="course.videoUrl" :autoplay="false"
@onPlay="handleVideoPlay" @onPause="handleVideoPause" />
</div>
<div v-if="course.type === 'audio'" class="w-full relative" style="border-bottom: 1px solid #F3F4F6;">
<!-- 音频播放器 -->
<AudioPlayer :songs="audioList" />
</div>
<!-- 标签页区域 -->
<div class="px-4 py-3 bg-white">
<van-tabs v-model:active="activeTab" sticky animated swipeable shrink @change="handleTabChange">
......@@ -38,7 +42,8 @@
</div>
<!-- 滚动区域:介绍和评论内容 -->
<div class="overflow-y-auto flex-1" :style="{paddingTop: topWrapperHeight, height: 'calc(100vh - ' + topWrapperHeight + ')'}">
<div class="overflow-y-auto flex-1"
:style="{ paddingTop: topWrapperHeight, height: 'calc(100vh - ' + topWrapperHeight + ')' }">
<div id="intro" class="py-4 px-4">
<h1 class="text-lg font-bold mb-2">{{ course.title }}</h1>
<div class="text-gray-500 text-sm flex items-center gap-2">
......@@ -56,20 +61,19 @@
<div class="text-gray-500 cursor-pointer text-sm" @click="showCommentPopup = true">查看更多</div>
</div>
<!-- 显示前三条评论 -->
<div v-for="comment in comments.slice(0, 3)" :key="comment.id" class="border-b border-gray-100 last:border-b-0 py-4">
<div v-for="comment in comments.slice(0, 3)" :key="comment.id"
class="border-b border-gray-100 last:border-b-0 py-4">
<div class="flex">
<img :src="comment.avatar" class="w-10 h-10 rounded-full flex-shrink-0" style="margin-right: 0.5rem;" />
<img :src="comment.avatar" class="w-10 h-10 rounded-full flex-shrink-0"
style="margin-right: 0.5rem;" />
<div class="flex-1 ml-3">
<div class="flex justify-between items-center mb-1">
<span class="font-medium text-gray-900">{{ comment.username }}</span>
<div class="flex items-center space-x-1">
<span class="text-sm text-gray-500">{{ comment.likes }}</span> &nbsp;
<van-icon
:name="comment.isLiked ? 'like' : 'like-o'"
:class="{'text-red-500': comment.isLiked, 'text-gray-400': !comment.isLiked}"
@click="toggleLike(comment)"
class="text-lg cursor-pointer"
/>
<van-icon :name="comment.isLiked ? 'like' : 'like-o'"
:class="{ 'text-red-500': comment.isLiked, 'text-gray-400': !comment.isLiked }"
@click="toggleLike(comment)" class="text-lg cursor-pointer" />
</div>
</div>
<p class="text-gray-700 text-sm mb-1">{{ comment.content }}</p>
......@@ -77,7 +81,8 @@
</div>
</div>
</div>
<van-popup v-model:show="showCommentPopup" position="bottom" round closeable safe-area-inset-bottom style="height: 80%">
<van-popup v-model:show="showCommentPopup" position="bottom" round closeable safe-area-inset-bottom
style="height: 80%">
<div class="flex flex-col h-full">
<!-- 固定头部 -->
<div class="flex-none px-4 py-3 border-b bg-white sticky top-0 z-10">
......@@ -87,20 +92,22 @@
<!-- 可滚动的评论列表 -->
<div class="flex-1 overflow-y-auto">
<div class="px-4 py-2 pb-16">
<div v-for="comment in comments" :key="comment.id" class="border-b border-gray-100 last:border-b-0 py-4">
<div v-for="comment in comments" :key="comment.id"
class="border-b border-gray-100 last:border-b-0 py-4">
<div class="flex">
<img :src="comment.avatar" class="w-10 h-10 rounded-full flex-shrink-0" style="margin-right: 0.5rem;" />
<img :src="comment.avatar" class="w-10 h-10 rounded-full flex-shrink-0"
style="margin-right: 0.5rem;" />
<div class="flex-1 ml-3">
<div class="flex justify-between items-center mb-1">
<span class="font-medium text-gray-900">{{ comment.username }}</span>
<span class="font-medium text-gray-900">{{ comment.username
}}</span>
<div class="flex items-center space-x-1">
<span class="text-sm text-gray-500">{{ comment.likes }}</span> &nbsp;
<van-icon
:name="comment.isLiked ? 'like' : 'like-o'"
:class="{'text-red-500': comment.isLiked, 'text-gray-400': !comment.isLiked}"
<span class="text-sm text-gray-500">{{ comment.likes }}</span>
&nbsp;
<van-icon :name="comment.isLiked ? 'like' : 'like-o'"
:class="{ 'text-red-500': comment.isLiked, 'text-gray-400': !comment.isLiked }"
@click="toggleLike(comment)"
class="text-lg cursor-pointer"
/>
class="text-lg cursor-pointer" />
</div>
</div>
<p class="text-gray-700 text-sm mb-1">{{ comment.content }}</p>
......@@ -114,19 +121,9 @@
<!-- 固定底部输入框 -->
<div class="flex-none border-t px-4 py-2 bg-white sticky bottom-0 z-10">
<div class="flex items-center space-x-2">
<van-field
v-model="popupComment"
rows="1"
autosize
type="textarea"
placeholder="请输入评论"
class="flex-1 bg-gray-100 rounded-lg"
/>
<van-button
type="primary"
size="small"
@click="submitPopupComment"
>发送</van-button>
<van-field v-model="popupComment" rows="1" autosize type="textarea"
placeholder="请输入评论" class="flex-1 bg-gray-100 rounded-lg" />
<van-button type="primary" size="small" @click="submitPopupComment">发送</van-button>
</div>
</div>
</div>
......@@ -135,15 +132,19 @@
</div>
<!-- 底部操作栏 -->
<div class="fixed bottom-0 left-0 right-0 bg-white border-t px-4 py-2 flex items-center space-x-4 bottom-wrapper">
<div class="flex-none flex flex-col items-center gap-1 cursor-pointer active:opacity-80" @click="showCatalog = true">
<div
class="fixed bottom-0 left-0 right-0 bg-white border-t px-4 py-2 flex items-center space-x-4 bottom-wrapper">
<div class="flex-none flex flex-col items-center gap-1 cursor-pointer active:opacity-80"
@click="showCatalog = true">
<van-icon name="bars" class="text-lg text-gray-600" />
<span class="text-xs text-gray-600">课程目录</span>
</div>
<div class="flex-grow flex-1 min-w-0">
<van-field v-model="newComment" rows="1" autosize type="textarea" placeholder="请输入留言" class="bg-gray-100 rounded-lg !p-0">
<van-field v-model="newComment" rows="1" autosize type="textarea" placeholder="请输入留言"
class="bg-gray-100 rounded-lg !p-0">
<template #input>
<textarea v-model="newComment" rows="1" placeholder="请输入留言" class="w-full h-full bg-transparent outline-none resize-none" />
<textarea v-model="newComment" rows="1" placeholder="请输入留言"
class="w-full h-full bg-transparent outline-none resize-none" />
</template>
</van-field>
</div>
......@@ -158,6 +159,7 @@ import { ref, onMounted, nextTick } from 'vue';
import { useRoute } from 'vue-router';
import { useTitle } from '@vueuse/core';
import VideoPlayer from '@/components/ui/VideoPlayer.vue';
import AudioPlayer from '@/components/ui/AudioPlayer.vue';
const route = useRoute();
const course = ref(null);
......@@ -245,6 +247,66 @@ const comments = ref([
}
]);
// 测试音频数据
const audioList = ref([
{
id: 1,
title: '示例音频 1',
artist: '演唱者 1',
cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg',
url: 'https://img.tukuppt.com/newpreview_music/09/03/95/5c8af46b01eb138909.mp3'
},
{
id: 2,
title: '示例音频 2',
artist: '演唱者 2',
cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg',
url: 'https://img.tukuppt.com/newpreview_music/08/99/00/5c88d4a8d1f5745026.mp3'
},
{
id: 3,
title: '示例音频 3',
artist: '演唱者 3',
cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg',
url: 'https://img.tukuppt.com/newpreview_music/09/03/95/5c8af46b01eb138909.mp3'
},
{
id: 4,
title: '示例音频 4',
artist: '演唱者 4',
cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg',
url: 'https://img.tukuppt.com/newpreview_music/09/03/72/5c8ad96fbf47328809.mp3'
},
{
id: 5,
title: '示例音频 5',
artist: '演唱者 5',
cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg',
url: 'https://img.tukuppt.com/newpreview_music/09/03/95/5c8af46b01eb138909.mp3'
},
{
id: 6,
title: '示例音频 6',
artist: '演唱者 6',
cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg',
url: 'https://img.tukuppt.com/newpreview_music/09/03/72/5c8ad96fbf47328809.mp3'
},
{
id: 7,
title: '示例音频 7',
artist: '演唱者 7',
cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg',
url: 'https://img.tukuppt.com/newpreview_music/09/03/95/5c8af46b01eb138909.mp3'
},
{
id: 8,
title: '示例音频 8',
artist: '演唱者 8',
cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg',
url: 'https://img.tukuppt.com/newpreview_music/09/03/72/5c8ad96fbf47328809.mp3'
},
]);
// 设置页面标题
useTitle('学习详情');
......@@ -294,8 +356,17 @@ onMounted(() => {
type: '视频课程',
studyCount: 1896,
cover: 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg',
date: '2024-12-04'
date: '2024-12-04',
type: 'video',
};
// TODO: 模拟数据音频和视频显示
console.warn(courseId);
if (courseId == '2') {
course.value.type = 'audio'
} else {
course.value.type = 'video'
}
}
})
......
......@@ -100,7 +100,7 @@ const columnCourses = ref([
{
id: 1,
title: "考前赋能冥想",
type: "视频",
type: "video",
thumbnail: "https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg",
progress: 100,
duration: 1200, // 20分钟
......@@ -109,7 +109,7 @@ const columnCourses = ref([
{
id: 2,
title: "开学礼·让的智慧·心法老师·20241001(上)",
type: "视频",
type: "audio",
thumbnail: "https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg",
progress: 1,
duration: 3600, // 1小时
......@@ -118,7 +118,7 @@ const columnCourses = ref([
{
id: 1,
title: "考前赋能冥想",
type: "视频",
type: "video",
thumbnail: "https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg",
progress: 100,
duration: 1200, // 20分钟
......@@ -127,7 +127,7 @@ const columnCourses = ref([
{
id: 2,
title: "开学礼·让的智慧·心法老师·20241001(上)",
type: "视频",
type: "video",
thumbnail: "https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg",
progress: 1,
duration: 3600, // 1小时
......@@ -139,7 +139,7 @@ const singleCourses = ref([
{
id: 3,
title: "冬季课·影响孩子命运的家族六要素·心法老师20250207",
type: "视频",
type: "video",
thumbnail: "https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg",
progress: 1,
duration: 3600,
......