完成大体功能和样式
This commit is contained in:
402
components/Carousel/Carousel.vue
Normal file
402
components/Carousel/Carousel.vue
Normal file
@@ -0,0 +1,402 @@
|
||||
<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: ''
|
||||
},
|
||||
vrList:{
|
||||
type: Array,
|
||||
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() {
|
||||
this.fillVrList();
|
||||
},
|
||||
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()
|
||||
if(this.vrList.length > 0) {
|
||||
console.log(this.vrViewPage)
|
||||
// this.$u.route({
|
||||
// url: this.vrViewPage,
|
||||
// params: {
|
||||
// title:"vr看资产",
|
||||
// vrList: this.vrList
|
||||
// }
|
||||
// })
|
||||
}
|
||||
},
|
||||
|
||||
/* 分区跳转 */
|
||||
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>
|
||||
@@ -2,23 +2,21 @@
|
||||
<view class="asset-list">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
:key="item.assetsNo"
|
||||
class="asset-card"
|
||||
@click="onClick(item)"
|
||||
>
|
||||
<image class="cover" :src="item.cover || '/static/images/default-cover.png'" mode="aspectFill" />
|
||||
<image class="cover" :src="'https://www.wujiaguotou.com' + item.coverImgUrl" mode="aspectFill" />
|
||||
|
||||
<view class="info">
|
||||
<text class="name">{{ item.name }}</text>
|
||||
<text class="address">{{ item.address }}</text>
|
||||
<text class="status">{{ item.statusText }}</text>
|
||||
<text class="name">{{ item.assetsName }}</text>
|
||||
<text class="address">{{ item.assetsAddress }}</text>
|
||||
</view>
|
||||
|
||||
<u-icon name="arrow-right" size="28" color="#999" class="arrow" />
|
||||
</view>
|
||||
|
||||
<u-loadmore v-if="showLoadmore" :status="loadStatus" />
|
||||
<u-empty v-else-if="list.length === 0" mode="list" text="暂无资产数据" />
|
||||
<u-empty v-if="list.length === 0" mode="list" text="暂无资产数据" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -29,17 +27,11 @@ export default {
|
||||
list: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
showLoadmore: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
loadStatus: {
|
||||
type: String,
|
||||
default: "loadmore",
|
||||
},
|
||||
}
|
||||
},
|
||||
emits: ["click"],
|
||||
onLoad(options) {
|
||||
},
|
||||
methods: {
|
||||
onClick(item) {
|
||||
this.$emit("click", item);
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="main-btn" @tap="onReserve">
|
||||
<text class="label">预约看房</text>
|
||||
<view class="main-btn" @tap="onReserve" :style="{backgroundColor: btnColor}">
|
||||
<text class="label">{{btnTitle || '加载错误'}}</text>
|
||||
</view>
|
||||
<share ref="share" :assetId="assetId" />
|
||||
<share ref="share" :assetId="assetId" :title="shareBtnTitle" :btnColor="btnColor"/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -25,7 +25,51 @@ import share from '../share/share.vue';
|
||||
export default {
|
||||
name: 'assetBottomBar',
|
||||
components: { share },
|
||||
props: { assetId: [String, Number], phone: String },
|
||||
props: {
|
||||
assetId: [String, Number],
|
||||
phone: String ,
|
||||
btnTitle: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
shareBtnTitle: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/** 小程序页面路径 */
|
||||
pagePath: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
|
||||
/** H5 页面路径 */
|
||||
h5Path: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
|
||||
/** 分享参数(id / code 等) */
|
||||
query: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
|
||||
/** 小程序码接口 */
|
||||
minicodeApi: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
/** H5 基础域名(可选) */
|
||||
baseUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
btnColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onShare() {
|
||||
console.log("调用分享")
|
||||
@@ -92,18 +136,15 @@ export default {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
/* 蓝色描边主按钮(完全居中) */
|
||||
.main-btn {
|
||||
flex-shrink: 0;
|
||||
width: 340rpx;
|
||||
height: 88rpx;
|
||||
border: 2rpx solid #FF2F31;
|
||||
border-radius: 44rpx;
|
||||
background-color: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #FF2F31;
|
||||
color: #ffffff;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
<template>
|
||||
<view :class="['my-card', customClass]" :style="cardStyle">
|
||||
<!-- 标题 -->
|
||||
<view v-if="title" class="my-card__header">
|
||||
{{ title }}
|
||||
</view>
|
||||
|
||||
<!-- 内容 -->
|
||||
<view class="my-card__body">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'mycard',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
margin: { // 外边距
|
||||
type: String,
|
||||
default: '20rpx'
|
||||
},
|
||||
borderRadius: { // 圆角
|
||||
type: String,
|
||||
default: '12rpx'
|
||||
},
|
||||
shadow: { // 是否显示阴影
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
customClass: { // 自定义类名
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
cardStyle() {
|
||||
return {
|
||||
margin: this.margin,
|
||||
borderRadius: this.borderRadius,
|
||||
boxShadow: this.shadow ? '0 4rpx 10rpx rgba(0,0,0,0.05)' : 'none',
|
||||
backgroundColor: '#fff',
|
||||
overflow: 'hidden'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.my-card__header {
|
||||
padding: 20rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.my-card__body {
|
||||
padding: 20rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -1,295 +0,0 @@
|
||||
<template>
|
||||
<view class="house-gallery">
|
||||
<!-- 图片轮播 -->
|
||||
<u-swiper
|
||||
:list="swiperList"
|
||||
:current="current"
|
||||
height="1200rpx"
|
||||
>
|
||||
<template #default="{ item, index }">
|
||||
<view class="slide-item">
|
||||
<image :src="item.url" mode="aspectFill" class="slide-img" />
|
||||
|
||||
<!-- VR 按钮 -->
|
||||
<view v-if="item.type === 'vr'" class="vr-btn" @click.stop="openVR">
|
||||
VR
|
||||
</view>
|
||||
|
||||
<!-- 视频按钮 -->
|
||||
<view v-if="item.type === 'video'" class="video-btn" @click.stop="playVideo">
|
||||
▶
|
||||
</view>
|
||||
|
||||
<!-- 图片索引 -->
|
||||
<view class="slide-index">{{ index + 1 }}/{{ swiperList.length }}</view>
|
||||
</view>
|
||||
</template>
|
||||
</u-swiper>
|
||||
|
||||
<!-- 广告遮罩栏 -->
|
||||
<view v-if="adVisible && adImage" class="ad-mask" @click="onAdClick">
|
||||
<image :src="adImage" mode="aspectFill" class="ad-img" />
|
||||
<view class="ad-close" @click.stop="adVisible = false">×</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部 Tab -->
|
||||
<view class="gallery-tab">
|
||||
<view
|
||||
class="gallery-tab-item"
|
||||
v-for="(item, i) in tabs"
|
||||
:key="i"
|
||||
:id="'tab-' + i"
|
||||
:class="{ active: activeTab === i }"
|
||||
@click="onTabClick(i)"
|
||||
>
|
||||
{{ item.label }}
|
||||
<text v-if="item.count">({{ item.count }})</text>
|
||||
</view>
|
||||
|
||||
<!-- 滑块动画 -->
|
||||
<view
|
||||
class="tab-line"
|
||||
:style="{ width: lineStyle.width + 'px', left: lineStyle.left + 'px' }"
|
||||
></view>
|
||||
</view>
|
||||
|
||||
<!-- 视频弹窗 -->
|
||||
<u-popup v-model="videoVisible" mode="center" border-radius="20">
|
||||
<video
|
||||
:src="videoUrl"
|
||||
autoplay
|
||||
controls
|
||||
style="width: 600rpx; height: 400rpx;"
|
||||
></video>
|
||||
</u-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "AssetGallery",
|
||||
props: {
|
||||
vrImage: String,
|
||||
videoCover: String,
|
||||
videoUrl: String,
|
||||
insideImages: Array,
|
||||
planImages: Array,
|
||||
adImage: String
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
current: 0,
|
||||
videoVisible: false,
|
||||
adVisible: true,
|
||||
tabs: [],
|
||||
indexMap: [],
|
||||
lineStyle: { width: 0, left: 0 }
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
setTimeout(() => this.updateTabLine(), 50);
|
||||
});
|
||||
},
|
||||
|
||||
watch: {
|
||||
activeTab() {
|
||||
this.$nextTick(() => this.updateTabLine());
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
swiperList() {
|
||||
let list = [];
|
||||
this.tabs = [];
|
||||
this.indexMap = [];
|
||||
let idx = 0;
|
||||
|
||||
if (this.vrImage) {
|
||||
this.tabs.push({ key: "vr", label: "VR", count: 1 });
|
||||
this.indexMap.push(idx);
|
||||
list.push({ type: "vr", url: this.vrImage });
|
||||
idx++;
|
||||
}
|
||||
|
||||
if (this.videoCover) {
|
||||
this.tabs.push({ key: "video", label: "视频", count: 1 });
|
||||
this.indexMap.push(idx);
|
||||
list.push({ type: "video", url: this.videoCover });
|
||||
idx++;
|
||||
}
|
||||
|
||||
if (this.insideImages?.length > 0) {
|
||||
this.tabs.push({ key: "inside", label: "内部图", count: this.insideImages.length });
|
||||
this.indexMap.push(idx);
|
||||
list.push(...this.insideImages.map((img) => ({ type: "inside", img })));
|
||||
idx += this.insideImages.length;
|
||||
}
|
||||
|
||||
if (this.planImages?.length > 0) {
|
||||
this.tabs.push({ key: "plan", label: "平面图", count: this.planImages.length });
|
||||
this.indexMap.push(idx);
|
||||
list.push(...this.planImages.map((url) => ({ type: "plan", url })));
|
||||
idx += this.planImages.length;
|
||||
}
|
||||
|
||||
return list;
|
||||
},
|
||||
|
||||
activeTab() {
|
||||
for (let i = 0; i < this.indexMap.length; i++) {
|
||||
const start = this.indexMap[i];
|
||||
const end = this.indexMap[i + 1] ?? 99999;
|
||||
if (this.current >= start && this.current < end) return i;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onAdClick() {
|
||||
this.$emit("ad-click");
|
||||
},
|
||||
|
||||
onSwiperChange(e) {
|
||||
this.current = e.detail.current;
|
||||
},
|
||||
|
||||
onTabClick(i) {
|
||||
this.current = this.indexMap[i];
|
||||
},
|
||||
|
||||
openVR() {
|
||||
this.$emit("open-vr");
|
||||
},
|
||||
|
||||
playVideo() {
|
||||
this.videoVisible = true;
|
||||
},
|
||||
|
||||
updateTabLine() {
|
||||
const index = this.activeTab;
|
||||
if (this.tabs.length === 0) return;
|
||||
|
||||
const id = `#tab-${index}`;
|
||||
uni.createSelectorQuery()
|
||||
.in(this)
|
||||
.select(id)
|
||||
.boundingClientRect((rect) => {
|
||||
if (!rect) return;
|
||||
this.lineStyle = { width: rect.width, left: rect.left };
|
||||
})
|
||||
.exec();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.house-gallery {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 轮播 */
|
||||
.slide-item {
|
||||
width: 100%;
|
||||
height: 1200rpx;
|
||||
position: relative;
|
||||
}
|
||||
.slide-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
.vr-btn,
|
||||
.video-btn {
|
||||
position: absolute;
|
||||
bottom: 40rpx;
|
||||
right: 40rpx;
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
.video-btn {
|
||||
font-size: 50rpx;
|
||||
}
|
||||
.slide-index {
|
||||
position: absolute;
|
||||
bottom: 20rpx;
|
||||
right: 20rpx;
|
||||
padding: 4rpx 8rpx;
|
||||
background: rgba(0,0,0,0.5);
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
/* 广告遮罩 */
|
||||
.ad-mask {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 120rpx;
|
||||
background: rgba(0,0,0,0.35);
|
||||
padding: 0 20rpx;
|
||||
z-index: 9;
|
||||
}
|
||||
.ad-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
.ad-close {
|
||||
position: absolute;
|
||||
right: 20rpx;
|
||||
top: 20rpx;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(0,0,0,0.55);
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
line-height: 40rpx;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
/* Tab bar */
|
||||
.gallery-tab {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
background: rgba(255,255,255,0.9);
|
||||
border-top: 1rpx solid #eee;
|
||||
height: 90rpx;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
z-index: 10;
|
||||
}
|
||||
.gallery-tab-item {
|
||||
padding: 20rpx 0;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
.gallery-tab-item.active {
|
||||
color: #2979ff;
|
||||
font-weight: bold;
|
||||
}
|
||||
.tab-line {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
height: 4rpx;
|
||||
background: #2979ff;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
</style>
|
||||
@@ -12,20 +12,18 @@
|
||||
<view class="nav-content">
|
||||
<!-- 左侧返回 -->
|
||||
<view class="nav-left">
|
||||
<u-icon
|
||||
name="arrow-left"
|
||||
size="44"
|
||||
:color="textColor"
|
||||
@click="onBack"
|
||||
v-if="showBack"
|
||||
></u-icon>
|
||||
<u-icon
|
||||
name="home"
|
||||
size="44"
|
||||
:color="textColor"
|
||||
@click="goHome"
|
||||
v-if="showHome"
|
||||
></u-icon>
|
||||
<view class="icon-btn" @click="onBack" v-if="showBack">
|
||||
<u-icon
|
||||
name="arrow-left"
|
||||
size="44"
|
||||
:color="textColor"
|
||||
|
||||
></u-icon>
|
||||
</view>
|
||||
|
||||
<view class="icon-btn" @click="goHome" v-if="showHome" :style="{marginLeft: 10 + 'rpx'}">
|
||||
<u-icon name="home" size="44" :color="textColor" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 中间标题 -->
|
||||
@@ -106,7 +104,7 @@ export default {
|
||||
},
|
||||
showHome: {
|
||||
type: Boolean,
|
||||
default: false // 回首页按钮默认不显示
|
||||
default: true // 回首页按钮默认不显示
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -155,15 +153,18 @@ export default {
|
||||
},
|
||||
|
||||
onBack() {
|
||||
if (this.back) {
|
||||
this.back();
|
||||
} else {
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length > 1) {
|
||||
uni.navigateBack();
|
||||
} else {
|
||||
uni.switchTab({
|
||||
url:'/pages/index/index'
|
||||
})
|
||||
}
|
||||
},
|
||||
goHome(){
|
||||
uni.reLaunch({
|
||||
url:'../index/index'
|
||||
uni.switchTab({
|
||||
url:'/pages/index/index'
|
||||
})
|
||||
},
|
||||
onExtraIconClick(index) {
|
||||
@@ -186,30 +187,41 @@ export default {
|
||||
.nav-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
.nav-left {
|
||||
width: 50rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.nav-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.nav-btn {
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
}
|
||||
.nav-left{
|
||||
width: 120rpx;
|
||||
}
|
||||
.nav-left,
|
||||
.nav-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 2;
|
||||
}
|
||||
.nav-title {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
max-width: 60%;
|
||||
text-align: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
z-index: 1;
|
||||
}
|
||||
// .nav-right {
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// .nav-btn {
|
||||
// margin-left: 20rpx;
|
||||
// }
|
||||
// }
|
||||
|
||||
// 透明状态样式
|
||||
&.transparent {
|
||||
|
||||
@@ -87,11 +87,14 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
onBack() {
|
||||
if (this.back) {
|
||||
this.back();
|
||||
} else {
|
||||
uni.navigateBack();
|
||||
}
|
||||
const pages = getCurrentPages();
|
||||
if (pages > 1) {
|
||||
uni.navigateBack();
|
||||
} else {
|
||||
uni.switchTab({
|
||||
url:'/pages/index/index'
|
||||
})
|
||||
}
|
||||
},
|
||||
goHome(){
|
||||
uni.reLaunch({
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
<template>
|
||||
<u-popup v-model="show" mode="bottom" border-radius="16" closeable>
|
||||
<view class="popup-content">
|
||||
<view class="popup-title">分享房源</view>
|
||||
<view class="popup-title">{{ title }}</view>
|
||||
|
||||
<u-cell-group>
|
||||
<!-- <u-cell-item title="📤 分享给微信好友" @click="shareToWeChat" /> -->
|
||||
<button open-type="share" class="share-btn">
|
||||
📤 分享给好友
|
||||
</button>
|
||||
<u-cell-item title="🔗 生成外部分享链接" @click="copyLink" />
|
||||
<u-cell-item title="📱 生成二维码分享" @click="generateQrcode" />
|
||||
<!-- 微信小程序:官方分享 -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<button open-type="share" class="share-btn" :style="{backgroundColor: btnColor}">
|
||||
📤 分享给好友
|
||||
</button>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- 非小程序:外部链接 -->
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<u-cell-item
|
||||
title="🔗 复制网页分享链接"
|
||||
@click="copyLink"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- 全平台 -->
|
||||
<!-- <u-cell-item
|
||||
title="📱 生成二维码分享"
|
||||
@click="generateQrcode"
|
||||
/> -->
|
||||
</u-cell-group>
|
||||
|
||||
<view class="cancel-btn" @click="show = false">取消</view>
|
||||
@@ -18,8 +32,8 @@
|
||||
<!-- 二维码弹窗 -->
|
||||
<u-popup v-model="showQrcode" mode="center" border-radius="12">
|
||||
<view class="qrcode-box">
|
||||
<image :src="qrcodeUrl" mode="widthFix" style="width: 300rpx;" />
|
||||
<view class="tips">扫一扫查看房源</view>
|
||||
<image v-if="qrcodeUrl" :src="qrcodeUrl" class="qrcode-img" />
|
||||
<view class="tips">扫一扫打开</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</u-popup>
|
||||
@@ -29,10 +43,55 @@
|
||||
import QRCode from 'qrcode'
|
||||
|
||||
export default {
|
||||
name: 'share',
|
||||
name: 'SharePopup',
|
||||
props: {
|
||||
assetId: [String, Number]
|
||||
/** 弹窗标题 */
|
||||
title: {
|
||||
type: String,
|
||||
default: '分享'
|
||||
},
|
||||
|
||||
/** 微信分享标题 */
|
||||
shareTitle: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
/** 小程序页面路径 */
|
||||
pagePath: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
|
||||
/** H5 页面路径 */
|
||||
h5Path: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
|
||||
/** 分享参数(id / code 等) */
|
||||
query: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
|
||||
/** 小程序码接口 */
|
||||
minicodeApi: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
/** H5 基础域名(可选) */
|
||||
baseUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
btnColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
@@ -40,70 +99,127 @@ export default {
|
||||
qrcodeUrl: ''
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
queryString() {
|
||||
const query = this.query || {}
|
||||
return Object.keys(query)
|
||||
.filter(key => query[key] !== undefined && query[key] !== null)
|
||||
.map(key => `${key}=${encodeURIComponent(query[key])}`)
|
||||
.join('&')
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
open() {
|
||||
console.log()
|
||||
this.show = true
|
||||
},
|
||||
shareToWeChat() {
|
||||
this.show = false
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.showToast({ title: '请使用右上角“···”转发好友或群', icon: 'none' })
|
||||
uni.showShareMenu({
|
||||
withShareTicket: true,
|
||||
menus: ['shareAppMessage', 'shareTimeline']
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef H5
|
||||
uni.showToast({ title: '请在微信内使用原生分享功能', icon: 'none' })
|
||||
// #endif
|
||||
},
|
||||
/** 复制网页链接 */
|
||||
copyLink() {
|
||||
this.show = false
|
||||
const shareUrl = `${window.location.origin}/#/pages/detail/assetsDetail?id=${this.assetId}`
|
||||
|
||||
let url = ''
|
||||
|
||||
// #ifdef H5
|
||||
url = `${window.location.origin}/#/${this.h5Path}?${this.queryString}`
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
url = `${this.baseUrl}/#/${this.h5Path}?${this.queryString}`
|
||||
// #endif
|
||||
|
||||
uni.setClipboardData({
|
||||
data: shareUrl,
|
||||
data: url,
|
||||
success: () => {
|
||||
uni.showToast({ title: '分享链接已复制', icon: 'success' })
|
||||
uni.showToast({ title: '链接已复制', icon: 'success' })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/** 生成二维码 / 小程序码 */
|
||||
async generateQrcode() {
|
||||
this.show = false
|
||||
const shareUrl = `${window.location.origin}/#/pages/detail/assetsDetail?id=${this.assetId}`
|
||||
this.showQrcode = true
|
||||
this.qrcodeUrl = await QRCode.toDataURL(shareUrl)
|
||||
this.qrcodeUrl = ''
|
||||
|
||||
// ========== 小程序 ==========
|
||||
// #ifdef MP-WEIXIN
|
||||
if (!this.minicodeApi) {
|
||||
uni.showToast({ title: '未配置小程序码接口', icon: 'none' })
|
||||
this.showQrcode = false
|
||||
return
|
||||
}
|
||||
|
||||
const res = await uni.request({
|
||||
url: this.minicodeApi,
|
||||
method: 'POST',
|
||||
data: {
|
||||
page: this.pagePath,
|
||||
scene: this.queryString
|
||||
}
|
||||
})
|
||||
|
||||
this.qrcodeUrl = res.data.url
|
||||
// #endif
|
||||
|
||||
// ========== H5 / App ==========
|
||||
// #ifndef MP-WEIXIN
|
||||
let url = ''
|
||||
|
||||
// #ifdef H5
|
||||
url = `${window.location.origin}/#/${this.h5Path}?${this.queryString}`
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
url = `${this.baseUrl}/#/${this.h5Path}?${this.queryString}`
|
||||
// #endif
|
||||
|
||||
this.qrcodeUrl = await QRCode.toDataURL(url, {
|
||||
width: 300,
|
||||
margin: 2
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
onShareAppMessage() {
|
||||
return {
|
||||
title: this.shareTitle || this.title,
|
||||
path: `/${this.pagePath}?${this.queryString}`
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.popup-content {
|
||||
padding: 30rpx;
|
||||
.popup-title {
|
||||
text-align: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
}
|
||||
.popup-title {
|
||||
text-align: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.share-btn {
|
||||
margin: 10rpx 30rpx;
|
||||
padding: 20rpx;
|
||||
border-radius: 12rpx;
|
||||
color: #fff;
|
||||
}
|
||||
.cancel-btn {
|
||||
text-align: center;
|
||||
font-size: 30rpx;
|
||||
color: #666;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
.qrcode-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 40rpx;
|
||||
.tips {
|
||||
margin-top: 20rpx;
|
||||
color: #666;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
text-align: center;
|
||||
}
|
||||
.qrcode-img {
|
||||
width: 300rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
<template>
|
||||
<view class="u-cell-item-plus" @click="$emit('click')">
|
||||
<!-- 左侧图标区域 -->
|
||||
<view class="u-cell-icon">
|
||||
<!-- 如果传入的是插槽,优先渲染插槽 -->
|
||||
<slot name="icon">
|
||||
<!-- 否则判断是图片还是内置图标 -->
|
||||
<template v-if="isImage(icon)">
|
||||
<image :src="icon" class="icon-img" mode="aspectFit"></image>
|
||||
</template>
|
||||
<template v-else-if="icon">
|
||||
<u-icon :name="icon" size="40"></u-icon>
|
||||
</template>
|
||||
</slot>
|
||||
</view>
|
||||
|
||||
<!-- 标题文字 -->
|
||||
<view class="u-cell-title">{{ title }}</view>
|
||||
|
||||
<!-- 右箭头 -->
|
||||
<view class="u-cell-arrow" v-if="arrow">
|
||||
<u-icon name="arrow-right"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "UCellItemPlus",
|
||||
props: {
|
||||
title: String,
|
||||
icon: String,
|
||||
arrow: Boolean,
|
||||
},
|
||||
methods: {
|
||||
isImage(src) {
|
||||
return src && (src.startsWith("http") || src.endsWith(".png") || src.endsWith(".jpg") || src.endsWith(".jpeg") || src.endsWith(".svg"));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.u-cell-item-plus {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx 32rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.u-cell-icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
margin-right: 20rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.icon-img {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.u-cell-title {
|
||||
flex: 1;
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.u-cell-arrow {
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user