vue+element大型表单解决方案(4)–锚点组件(下)

系列文章:

  • vue+element大型表单解决方案(1)–概览
  • vue+element大型表单解决方案(2)–表单拆分
  • vue+element大型表单解决方案(3)–锚点组件(上)

前言

上一篇基本实现了锚点组件的功能,还剩一些优化和功能升级留在这一篇中完成。首先是把样式优化下,使得接近百度百科的样式效果;其次在使用组件时用到了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;
  }
}
 

此时的效果如下:

image.png

下面正式开始。

样式优化

目标样式的左侧有一个节点导轨,导轨的上下两端各有一个空心的圆圈;各主节点相应的会有一个实心的圆圈;当前节点在导轨上有个三角指示。
在原来.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;
  }
}
 

此时效果如下:

image.png

样式部分没有什么特别好说的,都是利用伪元素在合适的位置绝对定位显示。

去掉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去掉了,测试后效果正常。

高亮锚点始终显示

现在的锚点还剩一个功能没有实现,就是左侧章节滚动时,高亮锚点可能自动显示在面板上。如下图:

image.png

怎么让锚点自动显示出来呢?显然要对当前锚点进行监听,当锚点变化时,计算当前锚点在锚点面板中的位置,如果处于中间偏下位置,则让其居于中间;如果锚点处于上半区,则直接让面板回到顶部。
增加的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'
    })
  })
}
 

此时就完全正常了。好了,在锚点上的时间够长了,下篇将回到表单的话题中去。谢谢您的阅读,欢迎提出指正意见!

(0)
上一篇 2021年5月26日 下午4:17
下一篇 2021年5月26日 下午4:32

相关推荐

发表回复

登录后才能评论