Files
RentWeAppFront/components/Carousel/Carousel.vue
2026-05-14 14:42:51 +08:00

378 lines
7.8 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}">
<!-- 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">
<!-- 图片 -->
<image v-if="item.mediaType==='image'" :src="item.src"
:class="['img', imageModeMap[index]==='contain'?'vertical':'']" @load="onImageLoad($event,index)"
@tap="onImageTap(index)" />
<!-- 视频 -->
<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>
</view>
<!-- VR -->
<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()" />
</view>
</swiper-item>
</swiper>
<!-- 自定义点指示器 -->
<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 v-if="zoomed" class="zoom-overlay" @tap="zoomed=false">
<image :src="zoomSrc" mode="widthFix" class="zoom-img" />
</view>
</view>
</template>
<script>
export default {
name: 'Carousel',
props: {
list: {
type: Array,
required: true
},
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,
playingIndex: null,
imageModeMap: {},
zoomed: false,
zoomSrc: '',
lastTap: 0
}
},
computed: {
normalizedList() {
const vr = [],
other = []
this.list.forEach(item => item.mediaType === 'vr' ? vr.push(item) : other.push(item))
return [...vr, ...other]
},
autoTypeIndicateList() {
const list = this.normalizedList
if (!list.length) return []
const result = []
let startIndex = 0,
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() {
return this.autoTypeIndicateList.length ? 100 / this.autoTypeIndicateList.length : 100
}
},
methods: {
onSwiperChange(e) {
this.current = e.detail.current
this.stopVideo()
},
// 图片模式判断
onImageLoad(e, index) {
const {
width,
height
} = e.detail
if (!width || !height) return
this.$set(this.imageModeMap, index, width >= height ? 'cover' : 'contain')
},
// 双击放大
onImageTap(index) {
const now = new Date().getTime()
if (now - this.lastTap < 300) {
this.zoomed = true
this.zoomSrc = this.normalizedList[index].src
}
this.lastTap = now
},
// 视频
playVideo(index) {
if (this.playingIndex === index) return
this.stopVideo()
this.playingIndex = index
uni.createVideoContext('video-' + index, this).play()
},
stopVideo() {
if (this.playingIndex !== null) {
uni.createVideoContext('video-' + this.playingIndex, this).pause()
this.playingIndex = null
}
},
onVideoPlay() {},
onVideoPause() {},
// VR
enterVR() {
this.stopVideo()
this.$u.route({
url: this.vrViewPage,
params: {
title: 'vr看资产',
id: this.assetsId
}
})
},
// 分区跳转
jumpToType(item) {
this.current = item.startIndex
},
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
}
}
}
</script>
<style scoped>
.carousel {
position: relative;
width: 100%;
height: 100%;
}
.swiper-container,
swiper-item {
width: 100%;
height: 100%;
}
.slide {
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 {
width: 100%;
height: 100%;
position: relative;
}
.video {
width: 100%;
height: 100%;
}
.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;
}
/* 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, 0.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, 0.4);
color: #fff;
font-size: 40rpx;
border-radius: 50%;
}
.arrow.left {
left: 20rpx;
}
.arrow.right {
right: 20rpx;
}
/* type bar */
.type-bar {
position: absolute;
bottom: 80rpx;
left: 50%;
transform: translateX(-50%);
display: flex;
height: 40rpx;
background: rgba(0, 0, 0, 0.4);
min-width: 300rpx;
overflow: hidden;
z-index: 10;
}
.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;
}
/* 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%;
}
</style>