feat(buyer): 商品视频内嵌相册展示(天猫风格)

- 视频作为相册第一项,进页静音自动循环播放,控制条可开声音
- 竖视频两侧黑边改为封面高斯模糊填充(覆盖 DPlayer 运行时注入的黑底)
- 缩略图显示视频第一帧,居中透明播放图标
- 悬停切换相册时自动暂停/续播,播放器容器常驻避免实例丢失
- 移除购买区观看视频按钮与弹窗
- isCollection 收藏状态查询补充 catch,避免未捕获报错
This commit is contained in:
pikachu1995@126.com
2026-10-28 15:09:52 +08:00
parent a55c8356c8
commit 21abd26b1f

View File

@@ -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>