383 行
9.8 KiB
Plaintext
383 行
9.8 KiB
Plaintext
<template>
|
||
<view class="page">
|
||
<uni-back-arrow-circle class="back" arrow-class="arrow"></uni-back-arrow-circle>
|
||
<swiper class="swiper" :current="current" :circular="index != 0" :vertical="true" @change="onSwiperChange"
|
||
@transition="onTransition">
|
||
<swiper-item class="swiper-item" v-for="(item,i) in visibleList" :key="i">
|
||
<video @click="changeState(i)" ref="video" class="video-box" :id="'video-'+i"
|
||
@loadstart="onLoadstart(i)" :src="item.src" :poster="item.poster_src" :autoplay="false"
|
||
:show-progress="false" :show-fullscreen-btn="false" :show-play-btn="false"
|
||
:show-center-play-btn="false" :loop="true" @play="onPlay(i)" @pause="onPause(i)" :http-cache="true"></video>
|
||
<view class="video-cover" @click="changeState(i)">
|
||
<image v-if="stateList[i] === 'pause'" class="play-btn" src="/static/template/swiper-vertical-video/play.png"
|
||
mode="widthFix"></image>
|
||
</view>
|
||
<view class="video-info" v-if="showDebugInfo">
|
||
<text class="video-info-text">容器:第 {{i}} 个</text>
|
||
<text class="video-info-text">内容:{{item.content}}</text>
|
||
</view>
|
||
<!-- 评论按钮 -->
|
||
<view class="comment-btn" @click="openCommentPanel">
|
||
<view class="comment-icon">
|
||
<text class="comment-icon-text">💬</text>
|
||
</view>
|
||
<text class="comment-count">{{commentCount}}</text>
|
||
</view>
|
||
</swiper-item>
|
||
</swiper>
|
||
<view class="debug-info" v-if="showDebugInfo">
|
||
<text class="status-text">debug-info 播放状态:</text>
|
||
<text class="status-text" v-for="(value,index) in stateList">第{{index+1}}个:{{ value }}</text>
|
||
</view>
|
||
|
||
<!-- 评论面板 -->
|
||
<comment-panel :show="showCommentPanel" @close="closeCommentPanel" @countChange="onCommentCountChange"></comment-panel>
|
||
</view>
|
||
</template>
|
||
<script setup lang="uts">
|
||
/**
|
||
* @description: 支持上下滑动切换短视频内容,并在当前视频上叠加评论入口的竖滑视频流页面。
|
||
* @logic: 页面只维护当前可见的 3 个视频容器,用户上下滑动 swiper 时更新可见数据和播放状态,点击评论按钮打开评论面板。
|
||
* @optimization: 通过只保留 3 个可见视频容器并复用数据窗口,降低连续视频流场景下的组件创建和视频上下文数量。
|
||
*/
|
||
|
||
type ListItem = { _id : string, content : string, src : string, poster_src : string }
|
||
let page : number = 0,
|
||
currentPageIsShow : boolean = true;
|
||
|
||
import { state, setNetless } from '@/store/index.uts'
|
||
import CommentPanel from './comment-panel.uvue'
|
||
|
||
const $videoContextMap = new Map<string, VideoContext>()
|
||
const list = ref([] as ListItem[])
|
||
const visibleList = ref([] as ListItem[])// 提高性能 可见的只有3个
|
||
const current = ref(0 as number)
|
||
const index = ref(0)
|
||
const stateList = ref(["pause", "unPlay", "unPlay"] as string[])
|
||
const showDebugInfo = ref(false as boolean)
|
||
|
||
// 评论相关
|
||
const showCommentPanel = ref(false as boolean)
|
||
const commentCount = ref(0 as number)
|
||
|
||
function openCommentPanel() {
|
||
showCommentPanel.value = true
|
||
}
|
||
|
||
function closeCommentPanel() {
|
||
showCommentPanel.value = false
|
||
}
|
||
|
||
function onCommentCountChange(count : number) {
|
||
commentCount.value = count
|
||
}
|
||
|
||
function getVideoContext(idx : number) : VideoContext {
|
||
let videoContext : VideoContext | null = $videoContextMap.get('video-' + idx)
|
||
if (videoContext == null) {
|
||
videoContext = uni.createVideoContext('video-' + idx) as VideoContext
|
||
$videoContextMap.set('video-' + idx, videoContext)
|
||
}
|
||
return videoContext
|
||
}
|
||
|
||
function doPlay(idx : number) {
|
||
console.log("doPlay video" + idx);
|
||
getVideoContext(idx).play()
|
||
}
|
||
|
||
function doStop(idx : number) {
|
||
console.log("doStop video-" + idx);
|
||
getVideoContext(idx).stop();
|
||
// TODO 临时方案:解决.stop()时触发了doPause的问题
|
||
setTimeout(() => {
|
||
stateList.value[idx] = 'unPlay'
|
||
}, 1000)
|
||
}
|
||
|
||
function doPause(idx : number) {
|
||
getVideoContext(idx).pause()
|
||
console.log("doPause video-" + idx);
|
||
}
|
||
|
||
function changeState(idx : number) {
|
||
if (stateList.value[idx] === 'play') {
|
||
doPause(idx)
|
||
} else {
|
||
doPlay(current.value)
|
||
}
|
||
}
|
||
|
||
function onLoadstart(idx : number) {
|
||
console.error("onLoadstart video" + idx);
|
||
}
|
||
|
||
function onPause(idx : number) {
|
||
stateList.value[idx] = 'pause'
|
||
console.log('onPause', idx);
|
||
}
|
||
|
||
function onPlay(idx : number) {
|
||
if (current.value != idx || !currentPageIsShow) {
|
||
onPause(idx)
|
||
} else {
|
||
stateList.value[idx] = 'play'
|
||
console.log('onPlay', idx);
|
||
}
|
||
}
|
||
|
||
function getData() : ListItem[] {
|
||
let videoUrlList = state.netless
|
||
? [
|
||
'/static/test-video/10second-demo.mp4',
|
||
'/static/test-video/10second-demo.mp4',
|
||
'/static/test-video/10second-demo.mp4'
|
||
]
|
||
: [
|
||
'https://web-ext-storage.dcloud.net.cn/uni-app-x/video/uts-5-16.mp4',
|
||
'https://web-ext-storage.dcloud.net.cn/uni-app-x/video/uni-ai-5-16.mp4',
|
||
'https://web-ext-storage.dcloud.net.cn/uni-app-x/video/uni-verify-5-16.mp4'
|
||
] as string[]
|
||
|
||
let posterSrcList = state.netless
|
||
? [
|
||
'/static/shuijiao.jpg',
|
||
'/static/logo.jpg',
|
||
'/static/shuijiao.jpg'
|
||
]
|
||
: [
|
||
'https://web-ext-storage.dcloud.net.cn/uni-app-x/dark-uni-uts-01.png',
|
||
'https://web-ext-storage.dcloud.net.cn/uni-app-x/dark-uni-ai-01.png',
|
||
'https://web-ext-storage.dcloud.net.cn/uni-app-x/dark-uni-verify-01.jpg'
|
||
] as string[]
|
||
|
||
|
||
let listData = [] as ListItem[];
|
||
for (let i = 0; i < 6; i++) {
|
||
let idx = list.value.length + i;
|
||
let listItem : ListItem = {
|
||
"_id": "a00" + idx,
|
||
"content": "这是第" + idx + "条数据,url地址" + videoUrlList[i % 3],
|
||
"src": videoUrlList[i % 3],
|
||
"poster_src": posterSrcList[i % 3]
|
||
}
|
||
listData.push(listItem)
|
||
}
|
||
return listData
|
||
}
|
||
|
||
function onSwiperChange(e : SwiperChangeEvent) {
|
||
// console.error('SwiperChangeEvent',e.detail.current);
|
||
current.value = e.detail.current;
|
||
}
|
||
|
||
function onTransition(/*e : SwiperTransitionEvent*/) {
|
||
// console.log('onTransition e.detail.dx', e.detail.dx);
|
||
}
|
||
|
||
watch(current, (curr : number, oldCurrent : number) => {
|
||
let changeNumber = curr - oldCurrent
|
||
if (changeNumber == 1 || changeNumber == -2) {
|
||
// console.error('向右');
|
||
index.value++
|
||
} else {
|
||
// console.error('向左');
|
||
index.value--
|
||
}
|
||
// //翻页(3项为一页)
|
||
if (Math.abs(changeNumber) == 2) {
|
||
// console.log('翻页');
|
||
page = Math.floor(index.value / 3);
|
||
// console.log(index.value);
|
||
// console.log('page',page);
|
||
// console.log('slice',3*page,3*page+3);
|
||
if (list.value.length < 3 * page + 3) {
|
||
let listData : ListItem[] = getData()
|
||
list.value.push(...listData)
|
||
}
|
||
|
||
let visible = list.value.slice(3 * page, 3 * page + 3)
|
||
// 换数据
|
||
visibleList.value = visible
|
||
}
|
||
|
||
stateList.value.forEach((_ : string, idx : number) => {
|
||
if (idx === curr) {
|
||
doPlay(curr)
|
||
} else {
|
||
// 除了选中的其他已经播放的都需要停止
|
||
doStop(idx)
|
||
console.log('index:' + idx + '已被执行停止');
|
||
}
|
||
})
|
||
})
|
||
|
||
// #ifdef APP-HARMONY
|
||
uni.getNetworkType({
|
||
success: (res) => {
|
||
setNetless(res.networkType === 'none')
|
||
// #endif
|
||
list.value = getData()
|
||
visibleList.value = list.value.slice(0, 3)
|
||
// #ifdef APP || MP
|
||
// 非web端,一启动完成,就播放第一个。因为 web端浏览器限制不支持自动播放
|
||
// 鸿蒙 vapor 模式下,v-for 后 nextTick 后 nativeView init 事件还没有触发,因此延时 30ms
|
||
nextTick(() => {
|
||
setTimeout(() => doPlay(0), 30)
|
||
})
|
||
// #endif
|
||
// #ifdef APP-HARMONY
|
||
}
|
||
})
|
||
// #endif
|
||
|
||
onPageShow(() => {
|
||
currentPageIsShow = true
|
||
})
|
||
|
||
onHide(() => {
|
||
currentPageIsShow = false
|
||
console.log('pages-onHide');
|
||
doPause(current.value)
|
||
})
|
||
|
||
onBackPress((): boolean => {
|
||
if (showCommentPanel.value) {
|
||
closeCommentPanel()
|
||
return true
|
||
}
|
||
return false
|
||
})
|
||
|
||
onUnload(() => {
|
||
doPause(current.value)
|
||
})
|
||
|
||
function jest_getStateList() : string[] {
|
||
return stateList.value
|
||
}
|
||
|
||
function jest_getCurrent() : number {
|
||
return current.value
|
||
}
|
||
|
||
function jest_isCommentPanelShown() : boolean {
|
||
return showCommentPanel.value
|
||
}
|
||
|
||
function jest_switchTo(idx : number) {
|
||
current.value = idx
|
||
}
|
||
|
||
defineExpose({
|
||
jest_getStateList,
|
||
jest_getCurrent,
|
||
jest_isCommentPanelShown,
|
||
jest_switchTo
|
||
})
|
||
</script>
|
||
|
||
<style>
|
||
.page {
|
||
flex: 1;
|
||
}
|
||
|
||
.swiper,
|
||
.swiper-item,
|
||
.video-box,
|
||
.video-cover {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
/* .swiper-item {
|
||
position: relative;
|
||
} */
|
||
|
||
.video-box {
|
||
width: 100%;
|
||
}
|
||
|
||
.video-cover {
|
||
position: absolute;
|
||
justify-content: center;
|
||
align-items: center;
|
||
align-content: center;
|
||
}
|
||
|
||
.play-btn {
|
||
width: 40px;
|
||
height: 40px;
|
||
}
|
||
|
||
.video-info {
|
||
position: absolute;
|
||
bottom: 0;
|
||
padding: 15px;
|
||
}
|
||
|
||
.video-info-text {
|
||
font-size: 14px;
|
||
color: red;
|
||
line-height: 20px;
|
||
}
|
||
|
||
.debug-info {
|
||
position: fixed;
|
||
top: 15px;
|
||
width: 100%;
|
||
background-color: rgba(255, 255, 255, 0.3);
|
||
}
|
||
|
||
.status-text {
|
||
color: red;
|
||
padding: 15px;
|
||
}
|
||
|
||
.back{
|
||
position: absolute;
|
||
/* #ifdef APP || MP */
|
||
top: var(--status-bar-height);
|
||
/* #endif */
|
||
/* #ifdef WEB */
|
||
top: 10px;
|
||
/* #endif */
|
||
z-index: 10;
|
||
}
|
||
|
||
/* uni-back-arrow-circle组件 自定义样式 */
|
||
.arrow {
|
||
border-left-color: #FFFFFF !important;
|
||
border-bottom-color: #FFFFFF !important;
|
||
}
|
||
|
||
/* 评论按钮 */
|
||
.comment-btn {
|
||
position: absolute;
|
||
right: 15px;
|
||
bottom: 80px;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
z-index: 10;
|
||
}
|
||
|
||
.comment-icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 20px;
|
||
background-color: rgba(0, 0, 0, 0.3);
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.comment-icon-text {
|
||
font-size: 20px;
|
||
}
|
||
|
||
.comment-count {
|
||
font-size: 12px;
|
||
color: #ffffff;
|
||
text-align: center;
|
||
}
|
||
</style>
|