feat: 集成音频播放组件和视频播放器
This commit is contained in:
@@ -5,38 +5,13 @@
|
||||
|
||||
<!-- 页面内容 -->
|
||||
<view class="page-content">
|
||||
<!-- 视频播放器 -->
|
||||
<view class="video-section">
|
||||
<VideoPlayer
|
||||
v-if="videoList.length > 0"
|
||||
ref="videoPlayerRef"
|
||||
v-model:current-index="currentVideoIndex"
|
||||
:video-list="videoList"
|
||||
:countdown-seconds="5"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 课程和章节信息 -->
|
||||
<view class="info-section">
|
||||
<view class="info-item">
|
||||
<text class="label">{{ $t('courseDetails.courseInfo') }}:</text>
|
||||
<text class="value">{{ courseTitle }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">{{ $t('courseDetails.chapterInfo') }}:</text>
|
||||
<text class="value">{{ chapterTitle }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 视频列表 -->
|
||||
<view v-if="videoList.length > 0" class="video-list-section">
|
||||
<view class="section-title">{{ $t('courseDetails.videoTeaching') }}</view>
|
||||
<wd-radio-group v-model="currentVideoIndex" shape="button" >
|
||||
<wd-radio v-for="(video, index) in videoList" :key="video.id" :value="index" class="mb-2!">
|
||||
【{{ video.type == 2 ? $t('courseDetails.audio') : $t('courseDetails.video') }}】{{ index + 1 }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<!-- 课程视频 -->
|
||||
<AliPlayer
|
||||
:video-list="videoList"
|
||||
:current-index="currentVideoIndex !== null ? currentVideoIndex : 0"
|
||||
:course="{courseTitle:courseTitle, chapterTitle: chapterTitle}"
|
||||
:cover="curriculumImgUrl || ''"
|
||||
/>
|
||||
|
||||
<!-- 选项卡 -->
|
||||
<wd-tabs v-model="currentTab" class="tabs-section" lineWidth="30">
|
||||
@@ -74,7 +49,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { courseApi } from '@/api/modules/course'
|
||||
import VideoPlayer from '@/components/video-player/index.vue'
|
||||
import AliPlayer from '@/components/ali-video/index.vue'
|
||||
import type { IChapterDetail } from '@/types/course'
|
||||
import type { IVideoInfo } from '@/types/video'
|
||||
|
||||
@@ -82,6 +57,7 @@ import type { IVideoInfo } from '@/types/video'
|
||||
const chapterId = ref<number>(0)
|
||||
const courseTitle = ref('')
|
||||
const chapterTitle = ref('')
|
||||
const curriculumImgUrl = ref('')
|
||||
|
||||
// 页面数据
|
||||
const chapterDetail = ref<IChapterDetail | null>(null)
|
||||
@@ -100,6 +76,7 @@ onLoad((options: any) => {
|
||||
chapterId.value = parseInt(options.id)
|
||||
courseTitle.value = options.courseTitle || ''
|
||||
chapterTitle.value = options.title || ''
|
||||
curriculumImgUrl.value = options.curriculumImgUrl || ''
|
||||
|
||||
loadChapterDetail()
|
||||
})
|
||||
|
||||
@@ -236,7 +236,7 @@ const handleToDetail = (chapter: IChapter, catalogue: ICatalogue) => {
|
||||
const noRecored = chapter.isAudition === 1 && catalogue.isBuy === 0 && !userVip.value
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages/course/details/chapter?id=${chapter.id}&courseId=${courseId.value}&courseTitle=${courseDetail.value?.title}&title=${chapter.title}&noRecored=${noRecored}`
|
||||
url: `/pages/course/details/chapter?id=${chapter.id}&courseId=${courseId.value}&courseTitle=${courseDetail.value?.title}&title=${chapter.title}&noRecored=${noRecored}&curriculumImgUrl=${courseDetail.value?.image}`
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
317
pages/index.vue
317
pages/index.vue
@@ -1,139 +1,190 @@
|
||||
<template>
|
||||
<div class="menu-container">
|
||||
|
||||
<!-- 一级导航 -->
|
||||
<div class="menu-level-1">
|
||||
<div
|
||||
v-for="item in level1"
|
||||
:key="item.id"
|
||||
class="menu-item"
|
||||
:class="{ active: selectedParentId === item.id }"
|
||||
@click="selectParent(item)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 二级导航区域 -->
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="childList.length"
|
||||
class="menu-level-2"
|
||||
:style="{ gridRowStart: childRowIndex }"
|
||||
>
|
||||
<div
|
||||
v-for="child in childList"
|
||||
:key="child.id"
|
||||
class="menu-item child"
|
||||
>
|
||||
{{ child.name }}
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
</div>
|
||||
<view>
|
||||
<yb-video ref="videoRef" title="测试视频" height="auto"
|
||||
:crossOrigin="crossOrigin"
|
||||
:src="src"
|
||||
:three="three"
|
||||
:danmu="danmu"
|
||||
:quality="quality"
|
||||
:subtitles="subtitles"
|
||||
:works="works"
|
||||
:workIndex="workIndex"
|
||||
:custom="custom"
|
||||
header
|
||||
controls
|
||||
@back="back"
|
||||
@workchange="handleWorkChange"
|
||||
@qualitychange="handleQualityChange"
|
||||
@loadedmetadata="handleLoaded">
|
||||
<view style="position: absolute;top:50px;left:50px;color:#fff" @tap="handleClickSlot">这是组件插槽仅在renderjs渲染类型下有效</view>
|
||||
</yb-video>
|
||||
<input class="danmu-input" v-model="text" type="text" placeholder="输入弹幕" />
|
||||
<button @tap="sendDanmu(1)">发送滚动弹幕</button>
|
||||
<button @tap="sendDanmu(5)">发送顶端弹幕</button>
|
||||
<button @tap="sendDanmu(4)">发送底端弹幕</button>
|
||||
<button @tap="toggle">播放/暂停</button>
|
||||
<button @tap="changeSrc(0)">切换3D</button>
|
||||
<button @tap="changeSrc(2)">切换2D</button>
|
||||
<button @tap="back">返回VUE2</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue"
|
||||
|
||||
// 模拟接口返回的一级导航
|
||||
const level1 = ref([
|
||||
{ id: 1, name: "导航1", children: [] },
|
||||
{ id: 2, name: "导航2", children: [
|
||||
{ id: 21, name: "子2-1" },
|
||||
{ id: 22, name: "子2-2" },
|
||||
{ id: 23, name: "子2-3" }
|
||||
]},
|
||||
{ id: 3, name: "导航3", children: [] },
|
||||
{ id: 4, name: "导航4", children: [
|
||||
{ id: 41, name: "子4-1" },
|
||||
{ id: 42, name: "子4-2" }
|
||||
]},
|
||||
{ id: 5, name: "导航5", children: [] },
|
||||
{ id: 6, name: "导航6", children: [
|
||||
{ id: 61, name: "子6-1" },
|
||||
{ id: 62, name: "子6-2" },
|
||||
{ id: 63, name: "子6-3" },
|
||||
{ id: 64, name: "子6-4" },
|
||||
]},
|
||||
{ id: 7, name: "导航7", children: [] },
|
||||
{ id: 8, name: "导航8", children: [] }
|
||||
])
|
||||
|
||||
// 选择的一级导航
|
||||
const selectedParentId = ref(null)
|
||||
|
||||
// 当前一级导航的子级
|
||||
const childList = computed(() => {
|
||||
const parent = level1.value.find(i => i.id === selectedParentId.value)
|
||||
return parent ? parent.children : []
|
||||
})
|
||||
|
||||
// 计算二级导航应该显示在哪一行(每行 4 个)
|
||||
const childRowIndex = computed(() => {
|
||||
if (!selectedParentId.value) return 3
|
||||
const index = level1.value.findIndex(i => i.id === selectedParentId.value)
|
||||
return Math.floor(index / 4) + 2 // 第一行 row=1,二级导航从 row=2 或 row=3
|
||||
})
|
||||
|
||||
const selectParent = (item) => {
|
||||
// 点击同一个时关闭
|
||||
if (selectedParentId.value === item.id) {
|
||||
selectedParentId.value = null
|
||||
} else {
|
||||
selectedParentId.value = item.id
|
||||
}
|
||||
}
|
||||
import { ref, nextTick } from 'vue';
|
||||
import { onReady } from '@dcloudio/uni-app'
|
||||
|
||||
const videoRef = ref('')
|
||||
|
||||
const src = ref('')
|
||||
const three = ref('')
|
||||
const crossOrigin = ref('')
|
||||
const danmu = ref([])
|
||||
const quality = ref([])
|
||||
const subtitles = ref([{
|
||||
title: 'ASS字幕',
|
||||
src: '/static/subtitle/[zmk.pw]我的世界大电影精译v3.ass'
|
||||
},{
|
||||
title: 'SRT字幕',
|
||||
src: '/static/subtitle/wednesday.1080p.web.h264-successfulcrab-hi.srt'
|
||||
},{
|
||||
title: 'VTT字幕',
|
||||
src: '/static/subtitle/sing-song_2025-09-05_103755.vtt'
|
||||
}])
|
||||
const works = ref([])
|
||||
const workIndex = ref(0)
|
||||
const custom = ref({
|
||||
header: {
|
||||
more: [{
|
||||
text: '测试按钮',
|
||||
click: () => {
|
||||
videoRef.value.showToast('点击了测试按钮')
|
||||
}
|
||||
}]
|
||||
},
|
||||
progress: {
|
||||
leftSlots: [{
|
||||
innerHTML: `
|
||||
<div class="yb-player-icon">测试按钮</div>
|
||||
`,
|
||||
click: () => {
|
||||
videoRef.value.showToast('点击了测试按钮')
|
||||
}
|
||||
}],
|
||||
rightSlots: [{
|
||||
innerHTML: `
|
||||
<div class="custom_danmu">
|
||||
<svg t="1756368791455" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4446" width="48" height="48"><path d="M628.01 470.03h88.851v71.575h-88.85V470.03zM628.01 361.426h88.851v69.107h-88.85v-69.107zM497.184 470.03h86.382v71.575h-86.382V470.03z" p-id="4447"></path><path d="M798.06 65.29H225.939c-81.839 0-148.204 68.834-148.204 153.746V795.52c0 84.93 67.835 163.189 151.559 163.189h565.393c83.724 0 151.577-78.277 151.577-163.189V219.036c0.001-84.911-66.344-153.746-148.202-153.746zM541.61 213.325c11.497 21.402 23.032 49.363 34.567 83.922-14.815 4.938-31.287 11.535-49.383 19.744-6.596-29.62-16.453-56.771-29.611-81.454l44.427-22.212zM400.927 586.04l-9.876 116.01c-3.318 32.91-17.718 52.644-43.201 59.26a631.2 631.2 0 0 1-77.75 14.796c-3.317-21.412-9.065-41.966-17.284-61.71 37.847 1.658 62.52 1.235 74.056-1.234 11.479-2.47 18.076-13.572 19.735-33.323l9.876-123.42h-118.48c6.558-52.651 9.875-109.413 9.875-170.321H358.97v-91.32c-54.32 0-97.125 0.848-128.357 2.469v-44.435c26.294 1.668 51.832 2.47 76.505 2.47h98.747a1257.144 1257.144 0 0 0-2.488 78.983c0 26.341 0.83 56.78 2.488 91.34H292.323l-4.938 88.86h120.93c-3.318 23.05-5.767 46.884-7.388 71.575z m397.398 101.205c-32.947-1.611-60.9-2.469-83.932-2.469h-86.382c0 37.877 0.79 74.045 2.469 108.614h-49.363c1.62-37.884 2.45-74.055 2.45-108.614H487.31c-26.33 1.67-49.363 3.318-69.116 4.948v-46.903c19.753 1.648 42.785 3.307 69.116 4.928h96.258v-66.648H447.821c1.62-41.108 2.469-85.543 2.469-133.286 0-47.715-0.848-88.86-2.47-123.418h180.19c14.816-34.558 27.952-69.918 39.506-106.145 18.058 8.256 37 15.664 56.753 22.223-16.474 18.123-32.08 46.084-46.895 83.922h86.382c-1.658 31.278-2.469 73.234-2.469 125.878 0 52.681 0.81 96.268 2.47 130.827H628.01v66.648h83.913c21.374 0 50.174-0.81 86.4-2.47v41.965z" p-id="4448"></path><path d="M497.184 361.426h86.382v69.107h-86.382v-69.107z" p-id="4449"></path></svg>
|
||||
</div>
|
||||
`,
|
||||
click: () => {
|
||||
videoRef.value.showToolbar({
|
||||
selector: '.custom_danmu',
|
||||
list: [{
|
||||
text: '自定义工具1',
|
||||
click: () => {
|
||||
videoRef.value.showToast('自定义工具1')
|
||||
}
|
||||
},{
|
||||
text: '自定义工具2',
|
||||
click: () => {
|
||||
videoRef.value.showToast('自定义工具2')
|
||||
}
|
||||
},{
|
||||
text: '自定义工具3',
|
||||
click: () => {
|
||||
videoRef.value.showToast('自定义工具3')
|
||||
}
|
||||
}]
|
||||
})
|
||||
}
|
||||
}]
|
||||
}
|
||||
})
|
||||
const text = ref('')
|
||||
onReady(() => {
|
||||
changeSrc(2)
|
||||
})
|
||||
const changeSrc = (index) => {
|
||||
const arr = [
|
||||
'/static/video/test-360.mp4',//如果网页开启了/h5/的前缀,就需要加上这个/h5/
|
||||
'https://v2-zj-scct.kwaicdn.com/upic/2025/08/14/20/BMjAyNTA4MTQyMDEwMDVfMzc5NDk1NDg1Nl8xNzIzNjYyNjIwNDdfMF8z_b_B92c62e9609ee404af820675b37447d6d.mp4?tag=1-1756166590-unknown-0-wo6mf8iejp-e4f1ab14369cda6a&provider=self&clientCacheKey=3x4mikyhc8uu4jk_b.mp4&di=b6859a4d&bp=14944&x-ks-ptid=172366262047&kwai-not-alloc=self-cdn&kcdntag=p:Sichuan;i:ChinaTelecom;ft:UNKNOWN;h:COLD;pn:kuaishouVideoProjection&Aecs=172.19.0.226&ocid=100000971&tt=b&ss=vps',
|
||||
'https://ydtnt-jw.oss-cn-zhangjiakou.aliyuncs.com/jw-video/%E8%A7%86%E9%A2%91/%E8%8B%B1%E8%AF%AD/%E5%94%90%E8%BF%9F%E8%AF%8D%E6%B1%87%E7%9A%84%E9%80%BB%E8%BE%91%E5%8D%95%E8%AF%8D%E8%AF%BE/Unit1-1_batch.mp4'
|
||||
]
|
||||
quality.value = []
|
||||
for ( let i = 0 ; i < 3; i++ ) {
|
||||
quality.value.push({
|
||||
title: i == 0 ? '480p' : i == 1 ? '720p' : '1080p',
|
||||
src: videoRef.value.parseSrc(arr[index]),
|
||||
type: 'video'
|
||||
})
|
||||
}
|
||||
works.value = []
|
||||
for ( let i = 0 ; i < 10; i++ ) {
|
||||
works.value.push({
|
||||
title: '第' + (i + 1) + '集',
|
||||
src: arr[index]
|
||||
})
|
||||
}
|
||||
workIndex.value = 0
|
||||
three.value = index == 0 ? '360' : 'none'
|
||||
crossOrigin.value = index == 0 ? 'anonymous' : ''
|
||||
src.value = arr[index]
|
||||
}
|
||||
|
||||
const handleWorkChange = (e) => {
|
||||
src.value = e.detail.src
|
||||
videoRef.value.showToast('由于测试切换链接和当前播放链接是同一个所以不会触发视频切换')
|
||||
}
|
||||
const handleQualityChange = (e) => {
|
||||
this.$refs.video.showToast('由于所有画质链接都是一样的,所以怎么切换都会索引到第一个画质')
|
||||
}
|
||||
|
||||
const handleLoaded = (e) => {
|
||||
if ( e.type == 'init' ) {
|
||||
danmu.value = []
|
||||
for (var i = 0 ; i < 8000; i++) {
|
||||
danmu.value.push({
|
||||
mode: 1,
|
||||
time: 1 * i / 2, // 弹幕出现的时间(单位:毫秒)
|
||||
text: '这是新增的一条弹幕' + i, // 弹幕文本内容
|
||||
color: '#0ff', // 该条弹幕的颜色,会覆盖全局设置
|
||||
})
|
||||
}
|
||||
nextTick(() => {
|
||||
videoRef.value.loadDanmu()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleClickSlot = () => {
|
||||
videoRef.value.showToast('点击了组件插槽')
|
||||
}
|
||||
|
||||
const toggle = () => {
|
||||
videoRef.value.toggle()
|
||||
}
|
||||
|
||||
const sendDanmu = (mode) => {
|
||||
if ( !text.value ) return
|
||||
videoRef.value.sendDanmu({
|
||||
mode,
|
||||
text: text.value,
|
||||
color: '#ffffff',
|
||||
fontSize: 18
|
||||
}, true)//true表示使用边框
|
||||
videoRef.value.showToast({message: '发送弹幕成功', duration: 5000})
|
||||
}
|
||||
|
||||
const back = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.menu-container {
|
||||
display: grid;
|
||||
grid-template-rows: auto auto auto;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 一级导航:4列布局 */
|
||||
.menu-level-1 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
background: #4a90e2;
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu-item.active {
|
||||
background: #2d73c7;
|
||||
}
|
||||
|
||||
/* 二级导航:自动换行 */
|
||||
.menu-level-2 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8px;
|
||||
background: #2d73c7;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.menu-item.child {
|
||||
background: #1e4f8a;
|
||||
}
|
||||
|
||||
/* 动画 */
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: all .2s ease;
|
||||
}
|
||||
.fade-enter-from, .fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.danmu-input {
|
||||
border: 1px solid #eee;
|
||||
height: 45px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -75,6 +75,9 @@
|
||||
<text v-if="item.hufenState" class="menu-list-hufen">{{hufenData?.total ?? 0}}<text
|
||||
style="margin-left: 6rpx;">湖分</text></text>
|
||||
</wd-cell>
|
||||
<wd-cell is-link @click="uni.navigateTo({
|
||||
url: '/pages/index'
|
||||
})">前往测试</wd-cell>
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
Reference in New Issue
Block a user