系列文章:
前言
上一篇基本实现了锚点组件的功能,还剩一些优化和功能升级留在这一篇中完成。首先是把样式优化下,使得接近百度百科的样式效果;其次在使用组件时用到了v-if="pageBlock"
这个判断,需要隐藏下细节;最后当锚点很多时,锚点要自动向可视范围内移动。
准备工作
要实现本篇的内容,首先要在表单组件公司信息后面增加一些章节,使得锚点数量足够,代码如下所示:
<div data-section="公司信息"></div>
<form2 ref="form2" :data="formDataMap.form2" />
<!-- 增加占位章节 -->
<div data-section="占位信息1" data-ismain></div>
<div data-section="xxx1"></div>
<div style="width:300px;height:200px;background:#ccc;">占位符</div>
<div data-section="xxx2"></div>
<div style="width:300px;height:200px;background:#ccc;">占位符</div>
<div data-section="xxx3"></div>
<div style="width:300px;height:200px;background:#ccc;">占位符</div>
<div data-section="占位信息2" data-ismain></div>
<div data-section="yyy1"></div>
<div style="width:300px;height:200px;background:#ccc;">占位符</div>
<div data-section="yyy2"></div>
<div style="width:300px;height:200px;background:#ccc;">占位符</div>
<div data-section="yyy3"></div>
<div style="width:300px;height:200px;background:#ccc;">占位符</div>
<div data-section="占位信息3" data-ismain></div>
<div data-section="zzz1"></div>
<div style="width:300px;height:200px;background:#ccc;">占位符</div>
<div data-section="zzz2"></div>
<div style="width:300px;height:200px;background:#ccc;">占位符</div>
<div data-section="zzz3"></div>
<div style="width:300px;height:200px;background:#ccc;">占位符</div>
复制代码
同时修改.form-wrapper
的样式,代码如下:
.form-wrapper {
position: relative;
width: 100%;
// 修改height为合适的演示高度
height: 500px;
// 增加背景色,主要是为了方便理解截图
background: #efefef;
padding: 16px;
overflow-y: auto;
::v-deep input {
width: 280px;
}
}
复制代码
此时的效果如下:
下面正式开始。
样式优化
目标样式的左侧有一个节点导轨,导轨的上下两端各有一个空心的圆圈;各主节点相应的会有一个实心的圆圈;当前节点在导轨上有个三角指示。
在原来.anchor
内部增加.anchor-track
,同时修改锚点的直接父节点为.anchor-list
,修改后的template代码如下:
<template>
<div class="anchor">
<div class="anchor-track"></div>
<div class="anchor-list">
<div v-for="node in sections" :key="node.label" :label="node.index"
:class="[node.ismain?'anchor-main-node':'anchor-sub-node',{'anchor-node-active':currentSection===node.label}]"
@click="handleClick(node.label)">
{{ node.label }}
</div>
</div>
</div>
</template>
复制代码
对应的样式代码如下:
.anchor {
position: relative;
width: 100%;
height: 100%;
}
.anchor-track {
position: absolute;
left: 4px;
top: -10px;
bottom: -10px;
width: 1px;
background: #aaa;
// 上下的空心圆圈
&::before {
content: '';
position: absolute;
top: 0;
left: -4px;
width: 10px;
height: 10px;
border-radius: 10px;
border: 1px solid #ccc;
background: #fff;
}
&::after {
content: '';
position: absolute;
bottom: 0;
left: -4px;
width: 10px;
height: 10px;
border-radius: 10px;
border: 1px solid #ccc;
background: #fff;
}
}
.anchor-list {
position: relative;
padding: 12px;
width: 100%;
height: 100%;
// 宽高尽量依赖外界容器
// 如果容器未处理,则使用默认最小值
min-width: 120px;
min-height: 120px;
overflow-x: visible;
overflow-y: auto;
// 隐藏滚动条
&::-webkit-scrollbar {
display: none;
}
}
.anchor-main-node {
position: relative;
margin: 8px 0;
font-size: 14px;
font-weight: bold;
color: #555;
cursor: pointer;
&::before {
content: attr(label);
margin-left: 6px;
margin-right: 6px;
}
// 新增实心点
&::after {
content: '';
position: absolute;
left: -11px;
top: 3px;
width: 8px;
height: 8px;
border-radius: 8px;
background: #666;
}
}
.anchor-sub-node {
position: relative;
margin: 8px 0;
padding-left: 22px;
font-size: 14px;
color: #666;
cursor: pointer;
&::before {
content: attr(label);
margin-right: 4px;
}
}
.anchor-node-active {
color: #38f;
// 新增三角
&::after {
content: '';
position: absolute;
left: -8px;
top: 0px;
width: 0px;
height: 0px;
border: 8px solid transparent;
border-left-color: #38f;
background: transparent;
border-radius: 0;
}
}
复制代码
此时效果如下:
样式部分没有什么特别好说的,都是利用伪元素在合适的位置绝对定位显示。
去掉v-if
在使用anchor组件时,为了防止anchormounted
时拿不到表单的dom结构,加上了v-if判断。这方案临时用用可以,如果做成正式组件就不太合适了,因此要想办法把这个v-if去掉。自然想到的是在组件内部watchpageBlock
这个属性,当oldValue为null
而newValue有值时,则这个状态是真正mounted的状态。但是这样做会破坏mounted的语义,增加了代码不可读性。我采用的办法是在anchor组件文件夹内,增加一层组件包装,把v-if在包装层实现。代码如下:
<template>
<anchor v-if="pageBlock" :page-block="pageBlock" />
</template>
<script>
import Anchor from './anchor'
export default {
components: {
Anchor
},
props: {
pageBlock: HTMLElement
}
}
</script>
<style scoped lang="scss">
</style>
复制代码
此时可以把表单组件内的v-if去掉了,测试后效果正常。
高亮锚点始终显示
现在的锚点还剩一个功能没有实现,就是左侧章节滚动时,高亮锚点可能自动显示在面板上。如下图:
怎么让锚点自动显示出来呢?显然要对当前锚点进行监听,当锚点变化时,计算当前锚点在锚点面板中的位置,如果处于中间偏下位置,则让其居于中间;如果锚点处于上半区,则直接让面板回到顶部。
增加的js如下:
watch: {
currentSection() {
this.showCurrentSectionsAnchor()
}
},
// mehthods里增加showCurrentSectionsAnchor方法
showCurrentSectionsAnchor() {
// 给锚点增加data-anchor属性,便于查找
const anchor = this.$refs['anchor'].querySelector(`[data-anchor=${this.currentSection}]`)
if (anchor) {
const wrapper = anchor.parentElement
const clientHeight = wrapper.clientHeight
const offsetTop = anchor.offsetTop
// 计算当前元素是否处于容器可视区域中间偏下的位置,如果是的,则让容器滚动使得元素可视居中
if (offsetTop > clientHeight / 2) {
wrapper.scrollTop = offsetTop - clientHeight / 2
} else {
wrapper.scrollTop = 0
}
}
}
复制代码
为了能根据锚点文字查找到锚点,给组件根元素.anchor
增加ref="anchor"
,在.anchor-list
内v-for生成锚点时,各节点绑定上:data-anchor="node.label"
。
测试效果,发现锚点会随着表单的滚动始终显示高亮的节点了。但是此时发生一个问题,当点击锚点时,左侧表单不会滚动了,怎么回事呢?我猜测是新增的showCurrentSectionsAnchor
方法内修改了锚点容器的scrollTop
,其本质也是一个滚动,这个滚动和handleClick
里的section.scrollIntoView
冲突了。既然如此,我给scrollIntoView
增加一个异步执行,避免同时滚动。修改后的handleClick
代码如下:
handleClick(label) {
// 设置当前锚点对应章节
this.currentSection = label
// 查找到到该章节的dom
const section = this.pageBlock.querySelector(`[data-section=${label}]`)
// 延迟执行
setTimeout(() => {
// 平滑滚动至该章节
section.scrollIntoView({
behavior: 'smooth',
block: 'start'
})
})
}
复制代码
此时就完全正常了。好了,在锚点上的时间够长了,下篇将回到表单的话题中去。谢谢您的阅读,欢迎提出指正意见!