预览效果

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>

使用方式

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

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

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();
			}
		}

页面滚动,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)
		}

父页面代码

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>

代码中出现的样式,如果出现效果异常,可根据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>
Logo

基于 Vue 的企业级 UI 组件库和中后台系统解决方案,为数万开发者服务。

更多推荐