Files
RentWeAppFront/components/Carousel/Carousel.vue

396 lines
8.1 KiB
Vue
Raw Normal View History

2026-01-15 17:18:24 +08:00
<template>
<view class="carousel" :style="{width:boxWidth,height:boxHeight}" @touchstart="touchStart" @touchmove="touchMove"
@touchend="touchEnd">
<!-- 轮播轨道 -->
<view class="track" :style="trackStyle">
<view class="slide" v-for="(item, index) in normalizedList" :key="index">
<!-- 图片 -->
<image v-if="item.mediaType === 'image'" :src="item.src" mode="aspectFill" />
<!-- 视频 -->
<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" />
<view v-if="playingIndex !== index" class="play-btn"></view>
</view>
<!-- VR -->
<view v-else-if="item.mediaType === 'vr'" class="vr-wrapper">
<image class="vr-cover" :src="item.src" mode="aspectFill" />
2026-01-30 09:01:38 +08:00
<image class="vr-icon" mode="aspectFit" :src="vrIcon" @tap.stop="enterVR()" />
2026-01-15 17:18:24 +08:00
</view>
</view>
</view>
<!-- 点指示器 -->
<view v-if="indicator === 'dot'" class="dots">
<view v-for="(item, index) in normalizedList" :key="index" class="dot"
:class="{ active: index === current }" />
</view>
<!-- 箭头指示器 -->
<view v-if="indicator === 'arrow'" class="arrow left" @click.stop="prev"></view>
<view v-if="indicator === 'arrow'" class="arrow right" @click.stop="next"></view>
<!-- 底部分区长条指示器固定分区长度跳动游标 -->
<view v-if="autoTypeIndicateList.length" class="type-bar">
<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 }}
</view>
</view>
<!-- 右下角索引 -->
<view class="index-display">
{{ current + 1 }} / {{ normalizedList.length }}
</view>
</view>
</template>
<script>
export default {
name: 'Carousel',
props: {
list: {
type: Array,
required: true
}, // [{src:'',type:''}]
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,
containerWidth: 0,
startX: 0,
offsetX: 0,
timer: null,
playingIndex: null
}
},
computed: {
/* VR 永远在最前 */
normalizedList() {
const vr = []
const other = []
this.list.forEach(item => {
item.mediaType === 'vr' ? vr.push(item) : other.push(item)
})
return [...vr, ...other]
},
trackStyle() {
return `width:${this.normalizedList.length * this.containerWidth}px;
transform: translate3d(${-this.current * this.containerWidth + this.offsetX}px,0,0);
transition:${this.offsetX === 0 ? 'transform .3s' : 'none'};`
},
/* 自动生成分区指示 */
autoTypeIndicateList() {
const list = this.normalizedList
if (!list.length) return []
const result = []
let startIndex = 0
let bizType = list[0].bizType
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() {
if (!this.autoTypeIndicateList.length) return 100
return 100 / this.autoTypeIndicateList.length
}
},
mounted() {
this.$nextTick(this.calcWidth)
this.start()
},
beforeUnmount() {
this.stop()
},
2026-01-30 09:01:38 +08:00
onLoad() {},
2026-01-15 17:18:24 +08:00
watch: {
normalizedList(list) {
if (this.current >= list.length) this.current = 0
},
current() {
this.stopVideo()
}
},
methods: {
calcWidth() {
uni.createSelectorQuery()
.in(this)
.select('.carousel')
.boundingClientRect(rect => {
if (rect) this.containerWidth = rect.width
})
.exec()
},
start() {
if (!this.autoplay) return
this.stop()
this.timer = setInterval(this.next, this.interval)
},
stop() {
this.timer && clearInterval(this.timer)
this.timer = null
},
next() {
this.current = (this.current + 1) % this.normalizedList.length
},
prev() {
this.current = (this.current - 1 + this.normalizedList.length) % this.normalizedList.length
},
touchStart(e) {
this.stop()
this.startX = e.touches[0].clientX
},
touchMove(e) {
this.offsetX = e.touches[0].clientX - this.startX
},
touchEnd() {
if (Math.abs(this.offsetX) > this.containerWidth / 4) {
this.offsetX > 0 ? this.prev() : this.next()
}
this.offsetX = 0
this.start()
},
/* 视频 */
playVideo(index) {
if (this.playingIndex === index) return
this.stopVideo()
this.playingIndex = index
this.stop()
this.$nextTick(() => {
uni.createVideoContext('video-' + index, this).play()
})
},
stopVideo() {
if (this.playingIndex !== null) {
uni.createVideoContext('video-' + this.playingIndex, this).pause()
this.playingIndex = null
}
},
onVideoPlay() {
this.stop()
},
onVideoPause() {
this.start()
},
/* VR */
enterVR() {
this.stop()
this.stopVideo()
2026-01-30 09:01:38 +08:00
this.$u.route({
url: this.vrViewPage,
params: {
title: "vr看资产",
id: this.assetsId
}
})
2026-01-15 17:18:24 +08:00
},
/* 分区跳转 */
jumpToType(item) {
this.stop()
this.stopVideo()
this.current = item.startIndex
this.$nextTick(this.start)
}
}
}
</script>
<style scoped>
.carousel {
overflow: hidden;
position: relative;
}
.track {
display: flex;
height: 100%;
}
.slide {
width: 100%;
flex-shrink: 0;
}
.slide image,
.video-wrapper,
.vr-wrapper,
video {
width: 100%;
height: 100%;
}
/* dots */
.dots {
position: absolute;
bottom: 20rpx;
left: 50%;
transform: translateX(-50%);
display: flex;
}
.dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: rgba(255, 255, 255, .5);
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;
background: rgba(0, 0, 0, .4);
color: #fff;
font-size: 40rpx;
border-radius: 50%;
}
.arrow.left {
left: 20rpx;
}
.arrow.right {
right: 20rpx;
}
/* video */
.video-wrapper {
position: relative;
}
.play-btn {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80rpx;
height: 80rpx;
line-height: 80rpx;
text-align: center;
border-radius: 50%;
background: rgba(0, 0, 0, .5);
color: #fff;
font-size: 40rpx;
}
/* vr */
.vr-wrapper {
position: relative;
}
.vr-icon {
2026-01-30 09:01:38 +08:00
position: absolute;
top: 50%;
left: 12%;
transform: translate(-50%, -50%);
width: 100rpx !important;
height: 100rpx !important;
background: rgba(80, 80, 80, 0.45);
border-radius: 50%;
padding: 12rpx;
2026-01-15 17:18:24 +08:00
}
.type-bar {
position: absolute;
bottom: 80rpx;
left: 50%;
transform: translateX(-50%);
height: 40rpx;
display: flex;
background: rgba(0, 0, 0, 0.4);
overflow: hidden;
min-width: 300rpx;
}
.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;
}
</style>