当前位置:   article > 正文

uniapp 微信小程序 tabs选项卡组件,可以设置固定顶部-吸顶(fixed),随页面滚动自动切换选项(仿淘宝详情),附带选项切换动画_uniapp tabs组件

uniapp tabs组件

预览效果

typeTabs组件代码

<template>
	<view>
		<scroll-view v-show="isShow" :class="{'fixed':isFixed}" scroll-x="true"
			class="scroll-view d-flex bg-white  position-relative">
			<view class="item text-center nowarp" v-for="(item,index) in sources" :key="index"
				:style="'width:'+(!isScroll?(100/sources.length)+'%':'auto')"
				:class="{'active font-weight-bold':type==item.type}" @click="clickType(item,index)">
				{{item.name}}
			</view>
			<!-- 滚动条 -->
			<view class="line-box" :style="'left:'+activeLeft+'px;'"></view>
		</scroll-view>
		<!-- 当固定顶部时,占位元素,高度可修改 -->
		<view v-show="isFixed && isShow" :style="'height:'+height+'px'"></view>
	</view>
</template>

<script>
	export default {
		name: "typeTabs",
		props: {
			sources: Array, //数据源
			isShow: { //是否显示(默认展示,不用可以删除)
				type: Boolean,
				default: true
			},
			isFixed: { //是否固定在顶部
				type: Boolean,
				default: false
			},
			isScroll: { //是否可以横向滚动  值false:每项宽度为(100/sources.length)%
				type: Boolean, //值true: 宽度自适应
				default: false
			},
			currentType: { //默认选项,不传为第一项
				type: Object,
				default: () => {
					{}
				}
			}
		},

		data() {
			return {
				activeLeft: 0, //滚动条的X值
				type: "", //当前选项
				height: "44"
			};
		},

		mounted: function() {
			//赋默认值
			this.type = this.currentType ? this.currentType.type : this.sources.length ? this.sources[0].type : ""
			setTimeout(() => {
				this.$nextTick(() => {
					this.getActiveElementY();
				})
			}, 200)
			if (this.isFixed) {
				uni.createSelectorQuery().in(this)
					.select(".fixed") //目标节点
					.boundingClientRect((target) => {
						if (target.height) {
							this.height = target.height;
						}
					})
					.exec();
			}
		},
		methods: {
			clickType(item, index, isUpate) {
				this.type = item.type;
				setTimeout(() => {
					if (!isUpate) {
						this.$emit("clickItem", item, index);
					}
					this.$nextTick(() => {
						this.getActiveElementY();
					})
				}, 200)
			},
			// 获取当前选项和scroll-view的scrollLeft值计算得出滚动条位置
			getActiveElementY(element = '.item.active') {
				let query = uni.createSelectorQuery().in(this);
				var promise1 = new Promise((resolve, reject) => {
					query.select(".scroll-view").fields({
						scrollOffset: true
					}, res => {
						if (res) {
							resolve(res.scrollLeft)
						}
						resolve(0)
					}).exec();
				})

				var promise2 = new Promise((resolve, reject) => {
					query.select(element).boundingClientRect(async res => {
						if (res) {
							resolve(res.left + (res.width / 2))
						}
						resolve(0)
					}).exec();
				})
				Promise.all([promise1, promise2].map(item => item.catch(error => ""))).then(res => {
					var left = 0
					res.map(item => {
						left += item
					})
					this.activeLeft = left;
				})

			}
		}
	}
</script>

<style lang="scss" scoped>
	.line-box {
		position: absolute;
		width: 56rpx;
		height: 4rpx;
		// background-color: #2B323C;
		background-color: #0D6ED9;
		// bottom: 48rpx;
		top: 74rpx;
		transform: translateX(-50%);
		transition: all 0.1s;
	}

	.item {
		display: inline-block;
		max-width: 400rpx;
		padding: 16rpx 40rpx;
	}

	.item.active {
		color: #0D6ED9;
	}

	.fixed {
		position: fixed;
		top: 0;
		/* #ifdef H5 */
		top: 44px;
		/* #endif */
		left: 0;
		right: 0;
		z-index: 2;
		box-shadow: 0rpx 2rpx 6rpx 0rpx rgba(0, 0, 0, 0.06);
	}
</style>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152

使用方式

<typeTabs ref="tabs" :sources="typeList" @clickItem="clickType" :isFixed="true" :isScroll="true"></typeTabs>
  • 1

点击选项,页面滚动到指定类型位置

methods: {
			clickType(item) {
				this.isScroll = true;
				var _this = this;
				uni.createSelectorQuery()
					.select(".container") //对应外层节点
					.boundingClientRect((container) => {
						uni.createSelectorQuery()
							.select("#" + item.type) //目标节点
							.boundingClientRect((target) => {

								uni.pageScrollTo({
									duration: 0, //过渡时间必须为0,uniapp bug,否则运行到手机会报错
									scrollTop: target.top - container.top -
										44, //滚动到实际距离是元素距离顶部的距离减去最外层盒子的滚动距离
									complete: ()=>{
										setTimeout(() => {
											_this.isScroll = false;
										},100)
									}
								});
								
								

							})
							.exec();
					})
					.exec();
			}
		}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

页面滚动,typeTabs滚动到指定的类型

this.$refs.tabs.clickType:调用子组件的方法,第三个值传true,防止页面点击回调多次出发,导致滚动错乱
if (event.scrollTop + 44 >= item.top) {:44可根据实际情况修改,也可以通过节点信息计算

onPageScroll: function(event) {
			if (this.isScroll || this.timer) return
			this.timer = setTimeout(() => {
				this.typeList.map((item, index) => {
					if (item.isActive) return;
					if (event.scrollTop + 44 >= item.top) {
						if (!item.isActive) {
							item.isActive = true;
							this.$refs.tabs.clickType(item, index, true);
							item.isActive = false;
						}
					}
				})
				this.timer = null;
			}, 50)
		}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

父页面代码

onReady:页面加载完成后根据ID获取对应选项在页面中的top值,用于页面滚动时的判断依据
onPageScroll:使用timer节流,防止计算量过大
isScroll: 用于防止选项点击事件与页面滚动事件冲突,如果有其他解决方案可以联系我

<template>
	<view class="container">
		<typeTabs ref="tabs" :sources="typeList" @clickItem="clickType" :isFixed="true" :isScroll="true"></typeTabs>
		<view class="content-box border-top-24" id="person">
			<view class="font-36 font-weight-bold title-box">服务对象</view>
			<view class="line">
				<view class="label-box">头像</view>
				<view>XXX</view>
			</view>
			<view class="line">
				<view class="label-box">姓名</view>
				<view>陈爷爷</view>
			</view>
			<view class="line">
				<view class="label-box">性别</view>
				<view></view>
			</view>
			<view class="line">
				<view class="label-box">身份证号</view>
				<view>234234234234213421X</view>
			</view>
			<view class="line">
				<view class="label-box">手机号</view>
				<view>XXXXXX</view>
			</view>
			<view class="line">
				<view class="label-box">居住地址</view>
				<view>XXXXXX</view>
			</view>
		</view>
		<view class="content-box border-top-24" id="project">
			<view class="font-36 font-weight-bold title-box">服务项目</view>
			<view class="line">
				<view class="label-box">服务项目</view>
				<view>XXXXXX</view>
			</view>
			<view class="line">
				<view class="label-box">标准时长</view>
				<view>XXXXXX</view>
			</view>
			<view class="line">
				<view class="label-box">服务费用</view>
				<view>XXXXXX</view>
			</view>
		</view>
		<view class="content-box border-top-24" id="info">
			<view class="font-36 font-weight-bold title-box">服务信息</view>
			<view class="line">
				<view class="label-box">开始时间</view>
				<view>2022年4月6日 10:46</view>
			</view>
			<view class="line">
				<view class="label-box">开始地址</view>
				<view>XXXXXX</view>
			</view>
			<view class="line">
				<view class="label-box">服务时长</view>
				<view>XXXXXX</view>
			</view>
			<view class="line">
				<view class="label-box">结束时间</view>
				<view>2022年4月6日 10:46</view>
			</view>
			<view class="line">
				<view class="label-box">结束地址</view>
				<view>XXXXXX</view>
			</view>
		</view>
		<view class="content-box border-top-24" id="video">
			<view class="font-36 font-weight-bold title-box border-bottom-2">录音录频</view>
			<view class="pt-40 d-flex font-24 desc-color">
				<view
					class="operation-box mr-32 d-flex flex-direction-column align-items-center justify-content-center">
					<image mode="widthFix" class="icon" src="@/static/images/audio.png" alt=""></image>
					<view class="mt-16">开始录音</view>
				</view>
				<view class="operation-box d-flex flex-direction-column align-items-center justify-content-center">
					<image mode="widthFix" class="icon" src="@/static/images/video.png" alt=""></image>
					<view class="mt-16">开始录频</view>
				</view>
			</view>
		</view>
		<view class="content-box border-top-24" id="release">
			<view class="font-36 font-weight-bold title-box border-bottom-2">服务晒单</view>
			<view class="pt-40">
				<textarea maxlength="-1" class="w-100" placeholder-class="placeholder server"
					placeholder="说说我的服务感受......" />
			</view>
		</view>
	</view>
</template>

<script>
	import typeTabs from "@/components/typeTabs/typeTabs.vue"
	export default {
		data() {
			return {
				timer: null,
				isShow: false,
				isFixed: false,
				isScroll: false,
				form: {},
				typeList: [{
					type: "person",
					name: "服务对象"
				}, {
					type: "project",
					name: "服务项目"
				}, {
					type: "info",
					name: "服务信息"
				}, {
					type: "video",
					name: "录音录频"
				}, {
					type: "release",
					name: "服务晒单"
				}],
			}
		},
		components: {
			typeTabs,
		},
		onReady: function() {
			this.typeList.map(async item => {
				item.top = await new Promise((reslove, reject) => {
					uni.createSelectorQuery()
						.select(".container") //对应外层节点
						.boundingClientRect((container) => {
							uni.createSelectorQuery()
								.select("#" + item.type) //目标节点
								.boundingClientRect((target) => {
									reslove(target.top)
								})
								.exec();
						})
						.exec();
				})
				return item;
			})
		},
		onPageScroll: function(event) {
			if (this.isScroll || this.timer) return
			this.timer = setTimeout(() => {
				this.typeList.map((item, index) => {
					if (item.isActive) return;
					if (event.scrollTop + 44 >= item.top) {
						if (!item.isActive) {
							item.isActive = true;
							this.$refs.tabs.clickType(item, index, true);
							item.isActive = false;
						}
					}
				})
				this.timer = null;
			}, 50)
		},
		methods: {
			clickType(item) {
				this.isScroll = true;
				var _this = this;
				uni.createSelectorQuery()
					.select(".container") //对应外层节点
					.boundingClientRect((container) => {
						uni.createSelectorQuery()
							.select("#" + item.type) //目标节点
							.boundingClientRect((target) => {

								uni.pageScrollTo({
									duration: 0, //过渡时间必须为0,uniapp bug,否则运行到手机会报错
									scrollTop: target.top - container.top -
										44, //滚动到实际距离是元素距离顶部的距离减去最外层盒子的滚动距离
									complete: ()=>{
										setTimeout(() => {
											_this.isScroll = false;
										},100)
									}
								});
								
								

							})
							.exec();
					})
					.exec();
			}
		}
	}
</script>

<style lang="scss" scoped>
	.content-box {
		padding: 0 40rpx;

		.title-box {
			padding: 28rpx 0;
		}

		.line {
			display: flex;
			align-items: center;
			justify-content: space-between;
			text-align: right;
			padding: 28rpx 0;
			border-top: 2rpx solid #F7F7FE;

			.head-image {
				width: 80rpx;
				height: 80rpx;
				border-radius: 8rpx;
			}

			.label-box {
				min-width: 160rpx;
				color: #556172;
				text-align: left;
			}

			.border-left-2 {
				border-left: 2rpx solid  #F7F7FE;
			}
		}

		.operation-box {
			width: 176rpx;
			height: 176rpx;
			background-color: #F6F7FA;
			border-radius: 8rpx;
			margin-right: 40rpx;
			margin-bottom: 40rpx;
			position: relative;
			overflow: hidden;

			image.icon {
				width: 52rpx;
			}

			image.close {
				width: 32rpx;
				position: absolute;
				right: 0;
				top: 0;
			}

			image.img-box {
				max-width: 100%;
			}
		}

	}

	.placeholder.server {
		font-size: 24rpx;
	}

	textarea {
		height: 440rpx;
	}
</style>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260

代码中出现的样式,如果出现效果异常,可根据class类名意思添加对应样式

<style>
.scroll-view{
	white-space: nowrap;
}
.font-weight-bold{
	font-weight: bold;
}
.nowarp {
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-line-clamp: 1;
    -webkit-box-orient: vertical;
}
.font-36 {
  font-size: 36rpx;
}
.d-flex {
  display: flex;
}

.d-inline-flex {
  display: inline-flex;
}

.border-bottom-2 {
  border-bottom: 2rpx solid $bg-color;
}

.border-top-24 {
  border-top: 24rpx solid $bg-color;
}

.mt-16 {
  margin-top: 16rpx;
}

.mr-16{
	margin-right: 16rpx;
}

.mr-32{
	margin-right: 32rpx;
}
.pl-40 {
	padding-left: 40rpx;
}
</style>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/312429
推荐阅读
相关标签
  

闽ICP备14008679号