2026-01-15 17:18:24 +08:00
|
|
|
|
<template>
|
2026-05-14 14:42:51 +08:00
|
|
|
|
<view class="carousel" :style="{width: boxWidth, height: boxHeight}">
|
|
|
|
|
|
<!-- Swiper 轮播 -->
|
|
|
|
|
|
<swiper :current="current" :autoplay="autoplay ? interval : 0" :interval="interval" :circular="true"
|
|
|
|
|
|
:indicator-dots="false" :display-multiple-items="1" @change="onSwiperChange" class="swiper-container">
|
|
|
|
|
|
<swiper-item v-for="(item, index) in normalizedList" :key="index" class="slide">
|
2026-01-15 17:18:24 +08:00
|
|
|
|
<!-- 图片 -->
|
2026-05-14 14:42:51 +08:00
|
|
|
|
<image v-if="item.mediaType==='image'" :src="item.src"
|
|
|
|
|
|
:class="['img', imageModeMap[index]==='contain'?'vertical':'']" @load="onImageLoad($event,index)"
|
|
|
|
|
|
@tap="onImageTap(index)" />
|
2026-01-15 17:18:24 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 视频 -->
|
2026-05-14 14:42:51 +08:00
|
|
|
|
<view v-else-if="item.mediaType==='video'" class="video-wrapper" @click.stop="playVideo(index)">
|
|
|
|
|
|
<video :id="'video-'+index" :src="item.src" :poster="item.poster" :controls="playingIndex===index"
|
|
|
|
|
|
:show-center-play-btn="false" object-fit="cover" @play="onVideoPlay" @pause="onVideoPause"
|
|
|
|
|
|
class="video" />
|
|
|
|
|
|
<view v-if="playingIndex!==index" class="play-btn">▶</view>
|
2026-01-15 17:18:24 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- VR -->
|
2026-05-14 14:42:51 +08:00
|
|
|
|
<view v-else-if="item.mediaType==='vr'" class="vr-wrapper">
|
|
|
|
|
|
<image class="vr-cover" :src="item.src" mode="aspectFit" />
|
|
|
|
|
|
<image class="vr-icon" :src="vrIcon" mode="aspectFit" @tap.stop="enterVR()" />
|
2026-01-15 17:18:24 +08:00
|
|
|
|
</view>
|
2026-05-14 14:42:51 +08:00
|
|
|
|
</swiper-item>
|
|
|
|
|
|
</swiper>
|
2026-01-15 17:18:24 +08:00
|
|
|
|
|
2026-05-14 14:42:51 +08:00
|
|
|
|
<!-- 自定义点指示器 -->
|
|
|
|
|
|
<view v-if="indicator==='dot'" class="dots">
|
|
|
|
|
|
<view v-for="(item,index) in normalizedList" :key="index" class="dot" :class="{active:index===current}" />
|
2026-01-15 17:18:24 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
|
2026-05-14 14:42:51 +08:00
|
|
|
|
<!-- 自定义箭头 -->
|
|
|
|
|
|
<view v-if="indicator==='arrow'" class="arrow left" @click.stop="prev">‹</view>
|
|
|
|
|
|
<view v-if="indicator==='arrow'" class="arrow right" @click.stop="next">›</view>
|
2026-01-15 17:18:24 +08:00
|
|
|
|
|
2026-05-14 14:42:51 +08:00
|
|
|
|
<!-- 底部分区指示器 -->
|
2026-01-15 17:18:24 +08:00
|
|
|
|
<view v-if="autoTypeIndicateList.length" class="type-bar">
|
2026-05-14 14:42:51 +08:00
|
|
|
|
<view v-for="(item,index) in autoTypeIndicateList" :key="index" class="type-segment"
|
|
|
|
|
|
:class="{active:current>=item.startIndex && current<=item.endIndex}"
|
|
|
|
|
|
:style="{width:getSegmentWidth+'%'}" @click.stop="jumpToType(item)">
|
|
|
|
|
|
{{item.name}}
|
2026-01-15 17:18:24 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2026-05-14 14:42:51 +08:00
|
|
|
|
|
2026-01-15 17:18:24 +08:00
|
|
|
|
<!-- 右下角索引 -->
|
2026-05-14 14:42:51 +08:00
|
|
|
|
<view class="index-display">{{current+1}} / {{normalizedList.length}}</view>
|
2026-01-15 17:18:24 +08:00
|
|
|
|
|
2026-05-14 14:42:51 +08:00
|
|
|
|
<!-- 双击放大 -->
|
|
|
|
|
|
<view v-if="zoomed" class="zoom-overlay" @tap="zoomed=false">
|
|
|
|
|
|
<image :src="zoomSrc" mode="widthFix" class="zoom-img" />
|
|
|
|
|
|
</view>
|
2026-01-15 17:18:24 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
export default {
|
|
|
|
|
|
name: 'Carousel',
|
|
|
|
|
|
props: {
|
|
|
|
|
|
list: {
|
|
|
|
|
|
type: Array,
|
|
|
|
|
|
required: true
|
2026-05-14 14:42:51 +08:00
|
|
|
|
},
|
2026-01-15 17:18:24 +08:00
|
|
|
|
indicator: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: 'dot'
|
|
|
|
|
|
}, // dot | arrow | none
|
|
|
|
|
|
autoplay: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: false
|
|
|
|
|
|
},
|
|
|
|
|
|
interval: {
|
|
|
|
|
|
type: Number,
|
|
|
|
|
|
default: 3000
|
|
|
|
|
|
},
|
|
|
|
|
|
vrIcon: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: ''
|
|
|
|
|
|
},
|
|
|
|
|
|
boxWidth: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: '100%'
|
|
|
|
|
|
},
|
|
|
|
|
|
boxHeight: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: '500rpx'
|
|
|
|
|
|
},
|
|
|
|
|
|
vrViewPage: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: ''
|
|
|
|
|
|
},
|
2026-01-30 09:01:38 +08:00
|
|
|
|
assetsId: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: ''
|
2026-01-15 17:18:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
current: 0,
|
2026-05-14 14:42:51 +08:00
|
|
|
|
playingIndex: null,
|
|
|
|
|
|
imageModeMap: {},
|
|
|
|
|
|
zoomed: false,
|
|
|
|
|
|
zoomSrc: '',
|
|
|
|
|
|
lastTap: 0
|
2026-01-15 17:18:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
computed: {
|
|
|
|
|
|
normalizedList() {
|
2026-05-14 14:42:51 +08:00
|
|
|
|
const vr = [],
|
|
|
|
|
|
other = []
|
|
|
|
|
|
this.list.forEach(item => item.mediaType === 'vr' ? vr.push(item) : other.push(item))
|
2026-01-15 17:18:24 +08:00
|
|
|
|
return [...vr, ...other]
|
|
|
|
|
|
},
|
|
|
|
|
|
autoTypeIndicateList() {
|
|
|
|
|
|
const list = this.normalizedList
|
|
|
|
|
|
if (!list.length) return []
|
|
|
|
|
|
const result = []
|
2026-05-14 14:42:51 +08:00
|
|
|
|
let startIndex = 0,
|
|
|
|
|
|
bizType = list[0].bizType
|
2026-01-15 17:18:24 +08:00
|
|
|
|
for (let i = 1; i <= list.length; i++) {
|
|
|
|
|
|
if (i === list.length || list[i].bizType !== bizType) {
|
|
|
|
|
|
result.push({
|
|
|
|
|
|
type: bizType,
|
|
|
|
|
|
name: bizType,
|
|
|
|
|
|
startIndex,
|
|
|
|
|
|
endIndex: i - 1
|
|
|
|
|
|
})
|
|
|
|
|
|
startIndex = i
|
|
|
|
|
|
bizType = list[i] && list[i].bizType
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return result
|
|
|
|
|
|
},
|
|
|
|
|
|
getSegmentWidth() {
|
2026-05-14 14:42:51 +08:00
|
|
|
|
return this.autoTypeIndicateList.length ? 100 / this.autoTypeIndicateList.length : 100
|
2026-01-15 17:18:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
2026-05-14 14:42:51 +08:00
|
|
|
|
onSwiperChange(e) {
|
|
|
|
|
|
this.current = e.detail.current
|
|
|
|
|
|
this.stopVideo()
|
2026-01-15 17:18:24 +08:00
|
|
|
|
},
|
2026-05-14 14:42:51 +08:00
|
|
|
|
// 图片模式判断
|
|
|
|
|
|
onImageLoad(e, index) {
|
|
|
|
|
|
const {
|
|
|
|
|
|
width,
|
|
|
|
|
|
height
|
|
|
|
|
|
} = e.detail
|
|
|
|
|
|
if (!width || !height) return
|
|
|
|
|
|
this.$set(this.imageModeMap, index, width >= height ? 'cover' : 'contain')
|
2026-01-15 17:18:24 +08:00
|
|
|
|
},
|
2026-05-14 14:42:51 +08:00
|
|
|
|
// 双击放大
|
|
|
|
|
|
onImageTap(index) {
|
|
|
|
|
|
const now = new Date().getTime()
|
|
|
|
|
|
if (now - this.lastTap < 300) {
|
|
|
|
|
|
this.zoomed = true
|
|
|
|
|
|
this.zoomSrc = this.normalizedList[index].src
|
2026-01-15 17:18:24 +08:00
|
|
|
|
}
|
2026-05-14 14:42:51 +08:00
|
|
|
|
this.lastTap = now
|
2026-01-15 17:18:24 +08:00
|
|
|
|
},
|
2026-05-14 14:42:51 +08:00
|
|
|
|
// 视频
|
2026-01-15 17:18:24 +08:00
|
|
|
|
playVideo(index) {
|
|
|
|
|
|
if (this.playingIndex === index) return
|
|
|
|
|
|
this.stopVideo()
|
|
|
|
|
|
this.playingIndex = index
|
2026-05-14 14:42:51 +08:00
|
|
|
|
uni.createVideoContext('video-' + index, this).play()
|
2026-01-15 17:18:24 +08:00
|
|
|
|
},
|
|
|
|
|
|
stopVideo() {
|
|
|
|
|
|
if (this.playingIndex !== null) {
|
|
|
|
|
|
uni.createVideoContext('video-' + this.playingIndex, this).pause()
|
|
|
|
|
|
this.playingIndex = null
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-05-14 14:42:51 +08:00
|
|
|
|
onVideoPlay() {},
|
|
|
|
|
|
onVideoPause() {},
|
|
|
|
|
|
// VR
|
2026-01-15 17:18:24 +08:00
|
|
|
|
enterVR() {
|
|
|
|
|
|
this.stopVideo()
|
2026-01-30 09:01:38 +08:00
|
|
|
|
this.$u.route({
|
|
|
|
|
|
url: this.vrViewPage,
|
|
|
|
|
|
params: {
|
2026-05-14 14:42:51 +08:00
|
|
|
|
title: 'vr看资产',
|
2026-01-30 09:01:38 +08:00
|
|
|
|
id: this.assetsId
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-01-15 17:18:24 +08:00
|
|
|
|
},
|
2026-05-14 14:42:51 +08:00
|
|
|
|
// 分区跳转
|
2026-01-15 17:18:24 +08:00
|
|
|
|
jumpToType(item) {
|
|
|
|
|
|
this.current = item.startIndex
|
2026-05-14 14:42:51 +08:00
|
|
|
|
},
|
|
|
|
|
|
prev() {
|
|
|
|
|
|
const len = this.normalizedList.length
|
|
|
|
|
|
this.current = (this.current - 1 + len) % len
|
|
|
|
|
|
},
|
|
|
|
|
|
next() {
|
|
|
|
|
|
const len = this.normalizedList.length
|
|
|
|
|
|
this.current = (this.current + 1) % len
|
2026-01-15 17:18:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.carousel {
|
|
|
|
|
|
position: relative;
|
2026-05-14 14:42:51 +08:00
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
2026-01-15 17:18:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-14 14:42:51 +08:00
|
|
|
|
.swiper-container,
|
|
|
|
|
|
swiper-item {
|
|
|
|
|
|
width: 100%;
|
2026-01-15 17:18:24 +08:00
|
|
|
|
height: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.slide {
|
2026-05-14 14:42:51 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.img {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.img.vertical {
|
|
|
|
|
|
object-fit: contain;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.video-wrapper {
|
2026-01-15 17:18:24 +08:00
|
|
|
|
width: 100%;
|
2026-05-14 14:42:51 +08:00
|
|
|
|
height: 100%;
|
|
|
|
|
|
position: relative;
|
2026-01-15 17:18:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-14 14:42:51 +08:00
|
|
|
|
.video {
|
2026-01-15 17:18:24 +08:00
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-14 14:42:51 +08:00
|
|
|
|
.play-btn {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
|
|
width: 80rpx;
|
|
|
|
|
|
height: 80rpx;
|
|
|
|
|
|
line-height: 80rpx;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.5);
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
font-size: 40rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.vr-wrapper {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.vr-icon {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
left: 12%;
|
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
|
|
width: 100rpx;
|
|
|
|
|
|
height: 100rpx;
|
|
|
|
|
|
background: rgba(80, 80, 80, 0.45);
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
padding: 12rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 17:18:24 +08:00
|
|
|
|
/* dots */
|
|
|
|
|
|
.dots {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
bottom: 20rpx;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dot {
|
|
|
|
|
|
width: 12rpx;
|
|
|
|
|
|
height: 12rpx;
|
|
|
|
|
|
border-radius: 50%;
|
2026-05-14 14:42:51 +08:00
|
|
|
|
background: rgba(255, 255, 255, 0.5);
|
2026-01-15 17:18:24 +08:00
|
|
|
|
margin: 0 6rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dot.active {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* arrows */
|
|
|
|
|
|
.arrow {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
|
width: 60rpx;
|
|
|
|
|
|
height: 60rpx;
|
|
|
|
|
|
line-height: 60rpx;
|
|
|
|
|
|
text-align: center;
|
2026-05-14 14:42:51 +08:00
|
|
|
|
background: rgba(0, 0, 0, 0.4);
|
2026-01-15 17:18:24 +08:00
|
|
|
|
color: #fff;
|
|
|
|
|
|
font-size: 40rpx;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.arrow.left {
|
|
|
|
|
|
left: 20rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.arrow.right {
|
|
|
|
|
|
right: 20rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-14 14:42:51 +08:00
|
|
|
|
/* type bar */
|
2026-01-15 17:18:24 +08:00
|
|
|
|
.type-bar {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
bottom: 80rpx;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
|
display: flex;
|
2026-05-14 14:42:51 +08:00
|
|
|
|
height: 40rpx;
|
2026-01-15 17:18:24 +08:00
|
|
|
|
background: rgba(0, 0, 0, 0.4);
|
|
|
|
|
|
min-width: 300rpx;
|
2026-05-14 14:42:51 +08:00
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
z-index: 10;
|
2026-01-15 17:18:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.type-segment {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
font-size: 20rpx;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding: 0 10rpx;
|
|
|
|
|
|
transition: background 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.type-segment.active {
|
|
|
|
|
|
background: rgba(255, 47, 49, 0.6);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.index-display {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
bottom: 80rpx;
|
|
|
|
|
|
right: 20rpx;
|
|
|
|
|
|
padding: 6rpx 12rpx;
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.5);
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
border-radius: 12rpx;
|
|
|
|
|
|
}
|
2026-05-14 14:42:51 +08:00
|
|
|
|
|
|
|
|
|
|
/* zoom */
|
|
|
|
|
|
.zoom-overlay {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.8);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
z-index: 999;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.zoom-img {
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
max-height: 100%;
|
|
|
|
|
|
}
|
2026-01-15 17:18:24 +08:00
|
|
|
|
</style>
|