mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-ui.git
synced 2026-11-13 20:38:54 +08:00
feat(buyer): 商品视频内嵌相册展示(天猫风格)
- 视频作为相册第一项,进页静音自动循环播放,控制条可开声音 - 竖视频两侧黑边改为封面高斯模糊填充(覆盖 DPlayer 运行时注入的黑底) - 缩略图显示视频第一帧,居中透明播放图标 - 悬停切换相册时自动暂停/续播,播放器容器常驻避免实例丢失 - 移除购买区观看视频按钮与弹窗 - isCollection 收藏状态查询补充 catch,避免未捕获报错
This commit is contained in:
@@ -3,27 +3,44 @@
|
|||||||
<div class="item-detail-show">
|
<div class="item-detail-show">
|
||||||
<!-- 详情左侧展示数据、图片,收藏、举报 -->
|
<!-- 详情左侧展示数据、图片,收藏、举报 -->
|
||||||
<div class="item-detail-left">
|
<div class="item-detail-left">
|
||||||
<!-- 大图、放大镜 -->
|
<!-- 大图、放大镜(视频商品首项为视频) -->
|
||||||
<!-- <div id="dplayer"></div> -->
|
<div class="item-detail-big-img" v-if="mediaList[mediaIndex]">
|
||||||
<div class="item-detail-big-img" v-if="imgList[imgIndex]">
|
|
||||||
<el-image
|
<el-image
|
||||||
|
v-show="!isCurrentVideo"
|
||||||
:src="currentImg"
|
:src="currentImg"
|
||||||
fit="cover"
|
fit="cover"
|
||||||
style="width: 100%; height: 100%"
|
style="width: 100%; height: 100%"
|
||||||
/>
|
/>
|
||||||
|
<div v-show="isCurrentVideo" class="goods-video-wrap">
|
||||||
|
<div class="goods-video-bg" :style="videoBgStyle"></div>
|
||||||
|
<div
|
||||||
|
v-show="hasVideo"
|
||||||
|
id="goodsVideoPlayer"
|
||||||
|
class="goods-video-player"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div id="dplayer"></div> -->
|
|
||||||
<div class="item-detail-img-row">
|
<div class="item-detail-img-row">
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="item-detail-img-small"
|
class="item-detail-img-small"
|
||||||
:class="{ 'is-active': imgIndex === index }"
|
:class="{ 'is-active': mediaIndex === index }"
|
||||||
@mouseover="imgIndex = index"
|
@mouseover="switchMedia(index)"
|
||||||
v-for="(item, index) in imgList"
|
v-for="(item, index) in mediaList"
|
||||||
:key="index"
|
:key="index"
|
||||||
>
|
>
|
||||||
<img :src="item.url || item"/>
|
<video
|
||||||
|
v-if="item.type === 'video'"
|
||||||
|
class="video-thumb"
|
||||||
|
:src="item.url"
|
||||||
|
muted
|
||||||
|
preload="metadata"
|
||||||
|
playsinline
|
||||||
|
></video>
|
||||||
|
<img v-else :src="mediaThumb(item)"/>
|
||||||
|
<span v-if="item.type === 'video'" class="video-badge">
|
||||||
|
<img :src="playIcon" alt="" />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -275,23 +292,17 @@
|
|||||||
show-icon
|
show-icon
|
||||||
:title="eCouponAfterSaleHint()"
|
:title="eCouponAfterSaleHint()"
|
||||||
/>
|
/>
|
||||||
<el-tooltip content="观看视频" v-if="skuDetail.goodsVideo">
|
|
||||||
<img class="view-video" @click="showGoodsVideo = true" :src="playIcon" alt="">
|
|
||||||
</el-tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-dialog title="浏览视频" v-model="showGoodsVideo">
|
|
||||||
<div id="dplayer"></div>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch, onMounted } from "vue";
|
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import { StarFilled } from '@element-plus/icons-vue';
|
import { StarFilled } from '@element-plus/icons-vue';
|
||||||
@@ -334,12 +345,11 @@ const emit = defineEmits(["handleClickSku"]);
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const showGoodsVideo = ref(false);
|
|
||||||
const goodsVideo = ref("");
|
|
||||||
const count = ref(1); // 商品数量
|
const count = ref(1); // 商品数量
|
||||||
const imgIndex = ref(0); // 展示图片下标
|
const mediaIndex = ref(0); // 展示媒体下标(相册图片/视频)
|
||||||
const currentSelceted = ref([]); // 当前商品sku
|
const currentSelceted = ref([]); // 当前商品sku
|
||||||
const imgList = ref([]); // 商品图片列表
|
const mediaList = ref([]); // 媒体列表:视频项在前,其后为图片字符串
|
||||||
|
let dp = null; // DPlayer 实例,有视频时进页即创建(静音自动播放)
|
||||||
const skuDetail = ref({
|
const skuDetail = ref({
|
||||||
specList: [],
|
specList: [],
|
||||||
}); // sku详情
|
}); // sku详情
|
||||||
@@ -355,9 +365,23 @@ const skuList = ref([]); // formatSku 中赋值(原 data 未声明)
|
|||||||
// startCheckout 通过字符串 key 操作对应 loading
|
// startCheckout 通过字符串 key 操作对应 loading
|
||||||
const loadings = { loading, loading1, loadingPintuan };
|
const loadings = { loading, loading1, loadingPintuan };
|
||||||
|
|
||||||
|
const currentMedia = computed(() => mediaList.value[mediaIndex.value] || null);
|
||||||
|
const isCurrentVideo = computed(() => currentMedia.value?.type === "video");
|
||||||
|
const hasVideo = computed(() =>
|
||||||
|
mediaList.value.some((item) => item.type === "video")
|
||||||
|
);
|
||||||
|
// 视频两侧用封面的高斯模糊填充,避免竖视频出现黑边
|
||||||
|
const videoBgStyle = computed(() => {
|
||||||
|
const video = mediaList.value.find((item) => item.type === "video");
|
||||||
|
return video && video.poster
|
||||||
|
? { backgroundImage: `url("${video.poster}")` }
|
||||||
|
: {};
|
||||||
|
});
|
||||||
|
|
||||||
const currentImg = computed(() => {
|
const currentImg = computed(() => {
|
||||||
const item = imgList.value[imgIndex.value];
|
const item = currentMedia.value;
|
||||||
return item ? item.url || item : "";
|
if (!item) return "";
|
||||||
|
return item.type === "video" ? item.poster : item.url || item;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -399,23 +423,46 @@ watch(
|
|||||||
{ deep: true, immediate: true }
|
{ deep: true, immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(showGoodsVideo, (val) => {
|
// 天猫/淘宝风格:进页即在相册首位静音循环播放,用户可通过控制条开声音
|
||||||
if (val) {
|
function initVideoPlayer() {
|
||||||
initVideo();
|
const video = mediaList.value[0];
|
||||||
}
|
if (!video || video.type !== "video") return;
|
||||||
});
|
nextTick(() => {
|
||||||
|
if (dp) return;
|
||||||
|
dp = new DPlayer({
|
||||||
|
container: document.getElementById("goodsVideoPlayer"),
|
||||||
|
autoplay: true,
|
||||||
|
loop: true,
|
||||||
|
muted: true,
|
||||||
|
video: {
|
||||||
|
url: video.url,
|
||||||
|
pic: video.poster,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化video
|
// 悬停切换相册项:离开视频时暂停,回到视频时续播
|
||||||
function initVideo() {
|
function switchMedia(index) {
|
||||||
if (!goodsVideo.value) {
|
if (index === mediaIndex.value) return;
|
||||||
setTimeout(() => {
|
const leavingVideo = isCurrentVideo.value;
|
||||||
goodsVideo.value = new DPlayer({
|
mediaIndex.value = index;
|
||||||
container: document.getElementById('dplayer'),
|
if (leavingVideo && dp) {
|
||||||
video: {
|
dp.pause();
|
||||||
url: skuDetail.value.goodsVideo,
|
} else if (isCurrentVideo.value && dp) {
|
||||||
},
|
dp.play();
|
||||||
});
|
}
|
||||||
}, 100);
|
}
|
||||||
|
|
||||||
|
// 缩略图:视频用封面,图片兼容字符串与 { url } 两种形态
|
||||||
|
function mediaThumb(item) {
|
||||||
|
return item.type === "video" ? item.poster : item.url || item;
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroyVideo() {
|
||||||
|
if (dp) {
|
||||||
|
dp.destroy();
|
||||||
|
dp = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -646,18 +693,29 @@ function promotion() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 相册里会混进规格 JSON,只留图片地址;没有相册时用主图
|
// 相册里会混进规格 JSON,只留图片地址;没有相册时用主图。有视频时视频排在相册第一项
|
||||||
function swiperGoodsImg() {
|
function swiperGoodsImg() {
|
||||||
|
destroyVideo();
|
||||||
|
mediaIndex.value = 0;
|
||||||
const source = skuDetail.value.goodsGalleryList || [];
|
const source = skuDetail.value.goodsGalleryList || [];
|
||||||
const gallery = Array.from(source).filter((item) => {
|
const gallery = Array.from(source).filter((item) => {
|
||||||
if (typeof item !== "string") return false;
|
if (typeof item !== "string") return false;
|
||||||
return item.indexOf('"url":') === -1 && item.indexOf('"status":') === -1;
|
return item.indexOf('"url":') === -1 && item.indexOf('"status":') === -1;
|
||||||
});
|
});
|
||||||
if (gallery.length) {
|
const images = gallery.length
|
||||||
imgList.value = gallery;
|
? gallery
|
||||||
return;
|
: skuDetail.value.original
|
||||||
|
? [skuDetail.value.original]
|
||||||
|
: [];
|
||||||
|
mediaList.value = [...images];
|
||||||
|
if (skuDetail.value.goodsVideo) {
|
||||||
|
mediaList.value.unshift({
|
||||||
|
type: "video",
|
||||||
|
url: skuDetail.value.goodsVideo,
|
||||||
|
poster: images[0] || "",
|
||||||
|
});
|
||||||
|
initVideoPlayer();
|
||||||
}
|
}
|
||||||
imgList.value = skuDetail.value.original ? [skuDetail.value.original] : [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -667,6 +725,8 @@ onMounted(() => {
|
|||||||
if (res.success && res.result) {
|
if (res.success && res.result) {
|
||||||
isCollected.value = true;
|
isCollected.value = true;
|
||||||
}
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
// 收藏状态为可选数据,失败时静默
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -674,6 +734,10 @@ onMounted(() => {
|
|||||||
// promotion();
|
// promotion();
|
||||||
document.title = skuDetail.value.goodsName;
|
document.title = skuDetail.value.goodsName;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
destroyVideo();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -737,10 +801,58 @@ onMounted(() => {
|
|||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#dplayer{
|
.goods-video-wrap {
|
||||||
width: 100%;
|
position: relative;
|
||||||
height: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-video-bg {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
filter: blur(28px) brightness(0.85);
|
||||||
|
transform: scale(1.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-video-player {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
/* 播放器默认黑底改为透明,露出毛玻璃背景 */
|
||||||
|
:deep(.dplayer),
|
||||||
|
:deep(.dplayer-video-wrap) {
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.6));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-thumb {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.item-detail-img-row {
|
.item-detail-img-row {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
@@ -750,6 +862,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.item-detail-img-small {
|
.item-detail-img-small {
|
||||||
|
position: relative;
|
||||||
width: 86px;
|
width: 86px;
|
||||||
height: 86px;
|
height: 86px;
|
||||||
border: 2px solid transparent;
|
border: 2px solid transparent;
|
||||||
@@ -1180,8 +1293,5 @@ onMounted(() => {
|
|||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
}
|
}
|
||||||
.view-video{
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
/******************商品图片及购买详情结束******************/
|
/******************商品图片及购买详情结束******************/
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user