Files
RentWeAppFront/components/Carousel/Carousel.vue

378 lines
7.8 KiB
Vue
Raw Normal View History

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>