better-scroll的使用

529 阅读2分钟

前言

最近在工作中遇到需要开发一个题号导航的需求,由于是用于移动端,为了更好的滑动体验,最终选用了使用比较多、比较成熟的better-scroll插件,根据具体需求场景封装,进行二次开发。

better-scorll

参考文章

题号导航面板

features

  • 支持收起和展开
  • 多层展示,一级菜单和二级菜单
  • 支持题号和题面的联动滚动,联动展开与收起

sourceCodeDemo js部分

	<template>
	<div
		class="question-menu-container"
		ref="wrapper"
		:class="unfold ? 'question-menu-container-unfold':''"
	>
		<ul
			class="question-container"
			ref="questionContainer"
		>
			<li
				ref="questionItem"
				:id="'questionNum' + (index + 1)"
				@click="switchQuestion(questionItem,index+1,questionItem.realIndex)"
				class="question-item"
				:class="[unfold ? 'question-item-unfold':'',haveAnswer(questionItem) ? 'completed' : '',currentQueNum == (index+1) ? 'selected' :'',getRightOrWrongClass(questionItem)]"
				v-for="(questionItem,index) in quesAllList"
				:key="index"
			>
				<span>{{questionItem.isBigWidthSmall?`${index+1}..`:index+1}}</span>
			</li>
			<!-- 一级展开时的二级弹框 start -->
			<div
				class="sub-question-container"
				id="sub-question-container"
				v-show="isShowSubQuetionMenuUnfold"
			>
				<div
					class="icon"
					id="icon"
				></div>
				<ul class="sub-question-wrapper">
					<li
						class="sub-question-item"
						:class="[unfold ? 'question-item-unfold':'',haveAnswer(item) ? 'completed' : '',currenSubIndex == index+1? 'selected' :'',getRightOrWrongClass(item)]"
						v-for="(item,index) in subQueArr"
						@click="subQueChange(item.realIndex)"
					>{{index+1}}</li>
				</ul>
			</div>
			<!-- 一级展开时的二级弹框 end -->
		</ul>
		<!-- 一级收起时的二级弹框 start -->
		<div
			class="sub-question-fold-container"
			id="sub-question-fold-container"
			v-show="isShowSubQuetionMenuFold"
		>
			<div class="left-icon"></div>
			<ul class="sub-question-wrapper">
				<li
					class="sub-question-item"
					:class="[unfold ? 'question-item-unfold':'',haveAnswer(item) ? 'completed' : '',currenSubIndex == index +1? 'selected' :'',getRightOrWrongClass(item)]"
					v-for="(item,index) in subQueArr"
					@click="subQueChange(item.realIndex)"
				>{{index+1}}</li>
			</ul>
		</div>
		<!-- 测试一级收起时的二级弹框 end -->
		<!-- 收起or展开 -->
		<a
			href="javascript:;"
			v-if="!unfold"
			class="operate fold"
			@click="unfoldOrFold()"
		>
			<div class="click-area"></div>
		</a>
		<i
			v-else
			class="operate unfold"
			@click="unfoldOrFold()"
		></i>

		<!-- 过渡层 -->
		<div
			v-show="isScrolled"
			class="transition-part"
		></div>
	</div>
</template>

<script>
	import BScroll from 'better-scroll'
	export default {
		props: {
			// 当前小题的题号
			currenSubIndex: {
				default: ''
			},
			quesAllList: {
				type: Array,
				default: () => []
			},
			currentSubmitAnswerArray: {
				type: Array,
				default: () => []
			},
			currentQuetionNum: {
				type: Number,
				default: 1
			}
		},
		created() { },
		mounted() {
			this.$nextTick(() => {
				setTimeout(() => {
					this.initScroll()
				}, 500)
			})
		},
		data() {
			return {
				subQueArr: [], 
				unfold: false, //展开,收起
				currentQueNum: this.currentQuetionNum,
				isScrolled: true,
				lineNum: 3,
				isShowSubQuetionMenuUnfold: false, //一级菜单展开时,控制二级菜单的收起与展开
				isShowSubQuetionMenuFold: false //一级菜单收起时,控制二级菜单的收起与展开

			}
		},
		methods: {
			haveAnswer(questionItem) {
			//xxx
			},
			getRightOrWrongClass: function (questionItem) {
				return questionItem.rightFlag ? 'right' : 'wrong';
			},
			unfoldOrFold: function () {
				this.unfold = !this.unfold
				this.initScroll()
				this.$emit('resize', this.unfold)
			},
			initScroll: function () {
				if (!this.$refs.wrapper) {
					return
				}
				this.calcScrollContainerHeight();
				let options = {
					y: 0,
					startY: 0,
					scrollX: false,
					scrollY: true,
					click: true,
					probeType: 2,
					preventFault: true,
					bounce: {
						left: false,
						right: false,
						top: true,
						bottom: true
					}
				}
				if (!this.scroll) {
					this.scroll = new BScroll(this.$refs.wrapper, options)
					this.scroll.on('scroll', (pos) => {
						this.isShowSubQuetionMenuFold = false;
					})
				} else {
					this.scroll.refresh()
				}
				//题号导航到上次选中的位置
				this.scroll.scrollToElement(
					'#questionNum' + this.currentQueNum,
					200,
					true,
					true
				)
			},
			calcScrollContainerHeight: function () {
				// 设置滚动容器的高度
				let height = 0
				let unfoldHeight = 0
				// 计算展开时的容器高度
				if (this.unfold) {
					if (this.currentSubmitAnswerArray.length <= 3) {
						unfoldHeight += this.$refs.questionItem[0].getBoundingClientRect()
							.height
					}
					let totalLine = this.currentSubmitAnswerArray.length / this.lineNum
					unfoldHeight +=
						this.$refs.questionItem[0].getBoundingClientRect().height *
						2 *
						totalLine +
						this.$refs.questionItem[0].getBoundingClientRect().height +
						(this.isShowSubQuetionMenuUnfold ? 200 : 0);
					this.$refs.questionContainer.style.height = unfoldHeight + 'px'
				} else {
					// 计算收起时容器的高度
					height +=
						this.currentSubmitAnswerArray.length *
						this.$refs.questionItem[0].getBoundingClientRect().height *
						2 +
						this.$refs.questionItem[0].getBoundingClientRect().height
					this.$refs.questionContainer.style.height = height + 'px'
				}
			},
			// 切换小题导航
			subQueChange(index) {
            //xxx
			},
			switchQuestion: function (questionItem, queIndex, queNum) {
				if (this.currentQueNum != queIndex) {
                	//xxx
				}
				//出现二级弹框
				if (this.unfold) {
					this.updateSubQuestionMenuUnfoldPosition(questionItem, queIndex, queNum);
				} else {
					this.updateSubQuestionMenuFoldPosition(questionItem, queIndex, queNum);
				}
				return false
			},
			updateSubQuestionMenuUnfoldPosition: function (questionItem, queIndex, queNum) {
				// 一级菜单展开时设置二级菜单的位置
				if (this.currentQueNum == queIndex) {
					if (questionItem.isBi) {
						this.isShowSubQuetionMenuUnfold = !this.isShowSubQuetionMenuUnfold;
						this.resizeSubQuestionMenuUnfoldPosition(queIndex);
					}
				} else {
					if (questionItem.isBigWidthSmall) {
						this.isShowSubQuetionMenuUnfold = false;
						this.resizeSubQuestionMenuUnfoldPosition(this.currentQueNum);
						this.isShowSubQuetionMenuUnfold = true;
						this.currentQueNum = queNum;
						this.resizeSubQuestionMenuUnfoldPosition(queIndex);
					} else {
						this.isShowSubQuetionMenuUnfold = false;
						this.resizeSubQuestionMenuUnfoldPosition(this.currentQueNum);
						this.currentQueNum = queNum;
					}
				}
			},
			resizeSubQuestionMenuUnfoldPosition: function (queIndex) {
				// 一级菜单展开时,重置二级菜单
				let currentElement = document.getElementById("questionNum" + queIndex);
				let subQuestionMenuElement = document.getElementById("sub-question-container");
				let subQuestionMenuIconElement = document.getElementById("icon");
				let temp = queIndex % 3;

				subQuestionMenuElement.style.top = currentElement.offsetTop + currentElement.getBoundingClientRect().height + 10 + 'px';
				subQuestionMenuIconElement.style.left = currentElement.getBoundingClientRect().left + Math.ceil(currentElement.getBoundingClientRect().width / 2) - 8 + 'px';
				let lastQuestionItemIndex = queIndex;
                
                /**
                *出现二级菜单,将下一行的题号设置marginTop,高度是二级菜单的高度
                */
                
				if (temp == 0) {
					for (let i = 0; i < 3; i++) {
						lastQuestionItemIndex++;
						let lastQuestionItem = document.getElementById("questionNum" + lastQuestionItemIndex);
						if (lastQuestionItem) {
							if (this.isShowSubQuetionMenuUnfold) {
								lastQuestionItem.style.marginTop = 200 + 'px';
							} else {
								lastQuestionItem.style.marginTop = 0 + 'px';
							}
						}
					}
				} else if (temp == 1) {
					for (let i = 0; i < 5; i++) {
						lastQuestionItemIndex++;
						if (i < 2) {
							continue;
						}
						let lastQuestionItem = document.getElementById("questionNum" + lastQuestionItemIndex);
						if (lastQuestionItem) {
							if (this.isShowSubQuetionMenuUnfold) {
								lastQuestionItem.style.marginTop = 200 + 'px';
							} else {
								lastQuestionItem.style.marginTop = 0 + 'px';
							}
						}
					}
				} else if (temp == 2) {
					for (let i = 0; i < 4; i++) {
						lastQuestionItemIndex++;
						if (i < 1) {
							continue;
						}
						let lastQuestionItem = document.getElementById("questionNum" + lastQuestionItemIndex);
						if (lastQuestionItem) {
							if (this.isShowSubQuetionMenuUnfold) {
								lastQuestionItem.style.marginTop = 200 + 'px';
							} else {
								lastQuestionItem.style.marginTop = 0 + 'px';
							}
						}
					}
				}

				this.calcScrollContainerHeight();
			},
			updateSubQuestionMenuFoldPosition: function (questionItem, queIndex, queNum) {
				// 一级菜单收起,设置二级菜单的位置
				if (this.currentQueNum == queIndex) {
					if (questionItem.isBigWidthSmall) {
						this.isShowSubQuetionMenuFold = !this.isShowSubQuetionMenuFold;
						if (this.isShowSubQuetionMenuFold) {
							this.resizeSubQuestionMenuFoldPosition(queIndex);
						}
					}
				} else {
					if (questionItem.isBigWidthSmall) {
						this.isShowSubQuetionMenuFold = true;
						this.currentQueNum = queNum;
						this.resizeSubQuestionMenuFoldPosition(this.currentQueNum);
					} else {
						this.isShowSubQuetionMenuFold = false;
						this.currentQueNum = queNum;
					}
				}
			},
			resizeSubQuestionMenuFoldPosition: function (queIndex) {
				// 一级菜单收起时,重置二级菜单
				let currentElement = document.getElementById("questionNum" + queIndex);
				let subQuestionMenuElement = document.getElementById("sub-question-fold-container");

				subQuestionMenuElement.style.left = 80 + 'px';
				subQuestionMenuElement.style.top = currentElement.getBoundingClientRect().top - 28 + 'px';
			},
			resetQuestionItemStyle: function () {
				// 初始化题号的样式
				let questionItemArr = Array.from(document.getElementsByClassName("question-item"));
				questionItemArr.forEach(item => {
					item.style.marginTop = 0 + 'px';
				});
			},
			renderQuestionNum: function (queNum) {
				this.$nextTick(() => {
					// 判断是否超出容器高度
				})
				return queNum
			}
		},
		watch: {
			currentQuetionNum(newVal, oldVal) {
				this.currentQueNum = newVal
				if (this.quesAllList[newVal - 1].isBigWidthSmall) {
					this.subQueArr = this.quesAllList[newVal - 1].subQues
					if (this.unfold) {
						this.isShowSubQuetionMenuUnfold = true
						this.resizeSubQuestionMenuUnfoldPosition(newVal)
					} else {
						this.isShowSubQuetionMenuFold = true;
						this.resizeSubQuestionMenuFoldPosition(newVal)
					}
				} else {
					this.resetQuestionItemStyle()
					this.isShowSubQuetionMenuUnfold = false;
					this.isShowSubQuetionMenuFold = false;
				}

			},
			unfold(newVal) {
				this.isShowSubQuetionMenuUnfold = false;
				this.isShowSubQuetionMenuFold = false;
				this.resetQuestionItemStyle();
			}
		}
	}
</script>

css部分

	<style lang="scss" scoped>
	.question-menu-container {
		overflow: hidden;
		position: relative;
		display: flex;
		flex-shrink: 0;
		flex-grow: 0;
		width: 80px;
		margin: 4px 0 20px;
		background: rgba(255, 255, 255, 1);
		box-shadow: 1px 1px 4px 0px rgba(0, 0, 0, 0.09);
		border-radius: 0px 20px 20px 0px;
		& > .question-container {
			padding: 24px 15px 0px;
			& > .question-item {
				position: relative;
				float: left;
				width: 40px;
				height: 34px;
				border-radius: 17px;
				border: 1px solid #8c8c8c;
				color: #666666;
				font-size: 15px;
				text-align: center;
				line-height: 30px;
				margin-bottom: 28px;
				.sub-que-wrap {
					position: absolute;
					padding: 24px 22px 0px;
					left: 0;
					& > .sub-que-item:nth-of-type(3n) {
						margin-right: 0;
					}

					.sub-que-item {
						float: left;
						width: 28px;
						height: 28px;
						margin-right: 36px;
						margin-bottom: 28px;
						border-radius: 15px;
						border: 1px solid #8c8c8c;
						color: #666666;
						font-size: 15px;
						text-align: center;
						line-height: 26px;
					}
				}
				&.question-item-unfold {
				}
				&.completed {
					background: #e5f8ff;
					border: 1px solid #00baff;
					color: #00baff;
				}
				&.selected {
					color: #ffffff;
					background: #00baff;
					border: 1px solid #00baff;
				}
				&.right {
					&::before {
						position: absolute;
						content: '';
						width: 14px;
						height: 14px;
						right: -4px;
						bottom: 0px;
						background: url(../../../../../assets/images/reportPicture/stuRight1.png)
							no-repeat center;
						background-size: contain;
					}
				}
				&.wrong {
					&::before {
						position: absolute;
						content: '';
						width: 14px;
						height: 14px;
						right: -4px;
						bottom: 0px;
						background: url(../../../../../assets/images/reportPicture/stuWrong1.png)
							no-repeat center;
						background-size: contain;
					}
				}
			}
			:nth-child(1) {
				// margin: rem(48px) 0px rem(56px) rem(44px);
			}
		}
		// 一级菜单收起时的二级菜单
		& > .sub-question-fold-container {
			position: fixed;
			z-index: 3;
			left: 0px;
			top: 0px;
			width: 224px;
			height: 200px;
			background: #ffffff;
			box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.15);
			border-radius: 20px;
			& > .left-icon {
				position: absolute;
				width: 10px;
				height: 10px;
				top: 38px;
				left: -5px;
				transform: rotate(45deg);
				background: #ffffff;
				box-shadow: 0px 0px 0px 0px rgba(0, 0, 0, 0.15);

				border-top: none;
				border-right: none;
			}
			& > .sub-question-wrapper {
				display: flex;
				flex-wrap: wrap;
				padding: 20px;
				& > .sub-question-item {
					position: relative;
					width: 30px;
					height: 30px;
					border-radius: 50%;
					border: 1px solid #cccccc;
					margin-right: 36px;
					margin-bottom: 28px;
					color: #595959;
					font-size: 15px;
					display: flex;
					align-items: center;
					justify-content: center;
					&.completed {
						background: #e5f8ff;
						border: 1px solid #00baff;
						color: #00baff;
					}
					&.selected {
						color: #ffffff;
						border: 1px solid #00baff;
						background: #00baff;
					}
					&.right {
						&::before {
							position: absolute;
							content: '';
							width: 14px;
							height: 14px;
							right: -4px;
							bottom: 0px;
							background: url(../../../../../assets/images/reportPicture/stuRight1.png)
								no-repeat center;
							background-size: contain;
						}
					}
					&.wrong {
						&::before {
							position: absolute;
							content: '';
							width: 14px;
							height: 14px;
							right: -4px;
							bottom: 0px;
							background: url(../../../../../assets/images/reportPicture/stuWrong1.png)
								no-repeat center;
							background-size: contain;
						}
					}
				}
				:nth-child(3n) {
					margin-right: 0px;
				}
			}
		}
		& > .operate {
			z-index: 5;
			position: absolute;
			top: 50%;
			transform: translateY(-50%);
			&.fold {
				width: 40px;
				height: 32px;
				right: 0px;
				& > div {
					height: 100%;
					position: relative;
					&::before {
						content: '';
						position: absolute;
						left: 75%;
						top: 50%;
						transform: translateY(-50%);
						width: 2px;
						border-radius: 1px;
						background: #b8dffa;
						height: 100%;
					}
				}
			}
			&.unfold {
				right: 7.5px;
				display: inline-block;
				width: 40px;
				height: 32px;
				background: url('../../../../../assets/images/answer/fold.png') no-repeat
					70% center;
				background-size: contain;
			}
		}
		& > .transition-part {
			position: absolute;
			bottom: 0px;
			left: 0px;
			width: 100%;
			height: 50px;
			background: linear-gradient(
				180deg,
				rgba(255, 255, 255, 0) 0%,
				rgba(255, 255, 255, 1) 100%
			);
			border-radius: 0px 0px 20px 0px;
		}
		//题号面板展开
		&.question-menu-container-unfold {
			width: 224px;
			& > .question-container {
				& > .question-item {
					margin-right: 36px;
				}
				// 一级题号菜单展开时的二级菜单
				& > .sub-question-container {
					position: absolute;
					z-index: 3;
					left: 0px;
					top: 0px;
					width: 224px;
					height: 200px;
					background: #f4f8fb;
					& > .icon {
						position: relative;
						width: 10px;
						height: 10px;
						top: -5px;
						left: 0px;
						transform: rotate(45deg);
						border: 1px solid #f4f8fb;
						background: #f4f8fb;
						border-bottom: none;
						border-right: none;
					}
					& > .sub-question-wrapper {
						display: flex;
						flex-wrap: wrap;
						padding: 20px;
						& > .sub-question-item {
							position: relative;
							width: 30px;
							height: 30px;
							border-radius: 50%;
							border: 1px solid #cccccc;
							margin-right: 36px;
							margin-bottom: 28px;
							color: #595959;
							font-size: 15px;
							display: flex;
							align-items: center;
							justify-content: center;
							&.completed {
								background: #e5f8ff;
								border: 1px solid #00baff;
								color: #00baff;
							}
							&.selected {
								color: #ffffff;
								border: 1px solid #00baff;
								background: #00baff;
							}
						}
						:nth-child(3n) {
							margin-right: 0px;
						}
					}
				}
				:nth-child(3n) {
					margin-right: 0px;
				}
			}
		}
	}
</style>

由于时间过于仓促,组件与业务的耦合性比较强,后续会与业务解耦合,增加通用性,欢迎各位评论或者进一步优化