Files
RentWeAppFront/components/Carousel/Carousel.vue
2026-01-30 09:01:38 +08:00

396 lines
8.1 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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" />
<image class="vr-icon" mode="aspectFit" :src="vrIcon" @tap.stop="enterVR()" />
</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: ''
},
assetsId: {
type: String,
default: ''
}
},
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()
},
onLoad() {},
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()
this.$u.route({
url: this.vrViewPage,
params: {
title: "vr看资产",
id: this.assetsId
}
})
},
/* 分区跳转 */
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 {
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;
}
.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>