Files
jiapuapp/components/family/VerticalVideoViewer.vue
T

209 lines
5.7 KiB
Vue

<template>
<view v-if="visible" class="vertical-video-viewer">
<view class="vertical-video-viewer__header">
<button aria-label="关闭上下滑动播放" @click="close">返回</button>
<text>{{ title }}</text>
<text>{{ activeIndex + 1 }}/{{ videos.length }}</text>
</view>
<swiper
class="vertical-video-viewer__swiper"
vertical
:current="activeIndex"
:duration="260"
@change="changeVideo"
>
<swiper-item v-for="(video, index) in videos" :key="video.id">
<view class="vertical-video-viewer__slide">
<video
:id="videoElementId(video)"
class="vertical-video-viewer__player"
:src="video.videoFile.accessUrl"
:poster="video.coverFile?.accessUrl || ''"
:autoplay="visible && index === activeIndex"
loop
controls
object-fit="contain"
/>
<view class="vertical-video-viewer__summary">
<text class="vertical-video-viewer__title">{{ video.title }}</text>
<text v-if="video.description" class="vertical-video-viewer__description">{{ video.description }}</text>
<view class="vertical-video-viewer__actions">
<button :disabled="actionBusy" @click="requestLike(video)">
{{ video.likedByCurrentUser ? "已赞" : "点赞" }} {{ video.likeCount || 0 }}
</button>
<button :disabled="actionBusy" @click="requestComments(video)">
评论 {{ video.commentCount || 0 }}
</button>
</view>
</view>
</view>
</swiper-item>
</swiper>
</view>
</template>
<script setup>
import { nextTick, ref, watch } from "vue";
const props = defineProps({
visible: { type: Boolean, default: false },
videos: { type: Array, default: () => [] },
initialVideoId: { type: [String, Number], default: "" },
title: { type: String, default: "视频播放" },
actionBusy: { type: Boolean, default: false },
});
const emit = defineEmits(["close", "like", "comments"]);
const activeIndex = ref(0);
const videoElementId = (video) => `vertical-video-${String(video.id)}`;
const pauseVideo = (index) => {
const video = props.videos[index];
if (!video) return;
uni.createVideoContext(videoElementId(video))?.pause();
};
const playVideo = (index) => {
const video = props.videos[index];
if (!video) return;
uni.createVideoContext(videoElementId(video))?.play();
};
const resolveInitialIndex = () => {
const requestedId = String(props.initialVideoId || "");
const requestedIndex = props.videos.findIndex(
(video) => String(video.id) === requestedId,
);
return requestedIndex >= 0 ? requestedIndex : 0;
};
watch(
() => props.visible,
async (visible) => {
if (!visible) {
pauseVideo(activeIndex.value);
return;
}
activeIndex.value = resolveInitialIndex();
await nextTick();
playVideo(activeIndex.value);
},
);
const changeVideo = async (event) => {
const nextIndex = Number(event?.detail?.current ?? 0);
if (!Number.isInteger(nextIndex) || nextIndex < 0 || nextIndex >= props.videos.length) return;
pauseVideo(activeIndex.value);
activeIndex.value = nextIndex;
await nextTick();
playVideo(activeIndex.value);
};
const close = () => {
pauseVideo(activeIndex.value);
emit("close");
};
const requestLike = (video) => emit("like", video);
const requestComments = (video) => {
pauseVideo(activeIndex.value);
emit("comments", video);
};
</script>
<style scoped lang="scss">
.vertical-video-viewer {
position: fixed;
z-index: 40;
inset: 0;
display: flex;
flex-direction: column;
color: #fff;
background: #080808;
}
.vertical-video-viewer__header {
display: grid;
min-height: 88rpx;
padding: calc(12rpx + env(safe-area-inset-top)) 24rpx 12rpx;
grid-template-columns: 120rpx 1fr 120rpx;
align-items: center;
background: rgba(0, 0, 0, 0.82);
text-align: center;
}
.vertical-video-viewer__header button,
.vertical-video-viewer__actions button {
min-height: 64rpx;
border: 1rpx solid rgba(255, 255, 255, 0.35);
border-radius: 32rpx;
color: #fff;
background: rgba(0, 0, 0, 0.42);
font-size: 26rpx;
line-height: 1;
}
.vertical-video-viewer__header button {
padding: 0 20rpx;
justify-self: start;
}
.vertical-video-viewer__header text:nth-child(2) {
overflow: hidden;
font-weight: 700;
text-overflow: ellipsis;
white-space: nowrap;
}
.vertical-video-viewer__header text:last-child {
justify-self: end;
color: rgba(255, 255, 255, 0.72);
}
.vertical-video-viewer__swiper,
.vertical-video-viewer__slide {
min-height: 0;
flex: 1;
}
.vertical-video-viewer__slide {
position: relative;
display: flex;
height: 100%;
align-items: center;
background: #080808;
}
.vertical-video-viewer__player {
width: 100%;
height: 100%;
}
.vertical-video-viewer__summary {
position: absolute;
right: 0;
bottom: 0;
left: 0;
padding: 80rpx 30rpx calc(28rpx + env(safe-area-inset-bottom));
background: linear-gradient(transparent, rgba(0, 0, 0, 0.86));
pointer-events: none;
}
.vertical-video-viewer__summary text {
display: block;
}
.vertical-video-viewer__title {
font-size: 34rpx;
font-weight: 700;
}
.vertical-video-viewer__description {
display: -webkit-box !important;
overflow: hidden;
margin-top: 12rpx;
color: rgba(255, 255, 255, 0.82);
font-size: 26rpx;
line-height: 1.55;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.vertical-video-viewer__actions {
display: flex;
margin-top: 22rpx;
gap: 16rpx;
pointer-events: auto;
}
.vertical-video-viewer__actions button {
min-width: 150rpx;
padding: 0 24rpx;
}
@media (prefers-reduced-motion: reduce) {
.vertical-video-viewer__swiper { scroll-behavior: auto; }
}
</style>