前言
最近在工作中遇到需要开发一个题号导航的需求,由于是用于移动端,为了更好的滑动体验,最终选用了使用比较多、比较成熟的better-scroll插件,根据具体需求场景封装,进行二次开发。
better-scorll
参考文章
- 目前最好用的前端滚动插件
- 解决各种移动端滚动场景的插件,借鉴idscroll,兼容iscroll,支持PC
题号导航面板
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>
由于时间过于仓促,组件与业务的耦合性比较强,后续会与业务解耦合,增加通用性,欢迎各位评论或者进一步优化