Skip to content

More Context Tip Animation ​

1. 实现效果 ​

1
2
3
4
5
6
7
more

2. 具体实现一(单文件应用) ​

该组件主要是为了在内容过多的时候提示用户进行下拉, 第一版是使用简单的ts代码和html标签进行硬处理, 考虑后面使用vue指令实现

原理是监听最后一个元素的可见度来控制动画的展示

html代码

html
<div class="wrapper view-demo-component">
  <div class="main">
    <div class="card">1</div>
    <div class="card">2</div>
    <div class="card">3</div>
    <div class="card">4</div>
    <div class="card">5</div>
    <div class="card">6</div>
    <div class="card">7</div>
  </div>
  <div class="tip">
    <div class="tip-text">more</div>
    <div class="tip-icon">
      <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
           class="bi bi-chevron-double-down" viewBox="0 0 16 16">
        <path fill-rule="evenodd"
              d="M1.646 6.646a.5.5 0 0 1 .708 0L8 12.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z"/>
        <path fill-rule="evenodd"
              d="M1.646 2.646a.5.5 0 0 1 .708 0L8 8.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z"/>
      </svg>
    </div>
  </div>
</div>

ts代码

typescript
const cards: HTMLCollectionOf<Element> = document.getElementsByClassName('card')
let card: Element = cards.item(cards.length - 1)
const tipEle = document.querySelector('.tip')
const ob = new IntersectionObserver(function(entries) {
    if (entries[0].intersectionRatio < 0.8) {
      tipEle.classList.contains('tip-hidden') && tipEle.classList.remove('tip-hidden')
    } else if (entries[0].intersectionRatio === 1) {
      !tipEle.classList.contains('tip-hidden') && tipEle.classList.add('tip-hidden')
    }
  }, {
    root: document.querySelector('.main'),
    threshold: [0.8, 1]
  }
)
ob.observe(card)

3. 具体实现二(基于creatApp添加指令) ​

在上一部分的末尾, 计划使用指令实现该功能, 因为之前研究不够深入, 经过一番搜索发现很难实现, 问了组内的大佬, 说这种需求的话不如写一个组件, 使用slot实现, 但是想着写就写了, 就当尝试下 所以就用了一些不太正常的方法写了下, 后面再把组件方式更新下

实现这里的时候找了许多方法, 但是都卡在了vnode创建后不能在编译父组件的时候插入这里, 使用了h函数, SFC编译等... 许多vue的底层原理还是要看一下才行 这里的实现有一些蠢, 其实差不多就是新写一个组件

demo组件 该组件绑定了v-more-context-tip 指令, 该指令需要传入两个参数, 以帮助指令内部判断对哪些元素进行监听

vue
<template>
  <div v-more-context-tip="{ parentRef: 'main', targetRef: 'card' }" class="wrapper view-demo-component">
    <div class="main">
      <div class="card">1</div>
      <div class="card">2</div>
      <div class="card">3</div>
      <div class="card">4</div>
      <div class="card">5</div>
      <div class="card">6</div>
      <div class="card">7</div>
    </div>
  </div>
</template>
<script setup lang="ts">
import { createApp } from "vue";
import MoreContextTip from './DirectiveComponent.vue'
const vMoreContextTip = {
  beforeMount: (el, binding) => {
    // 由于实例挂载的时候会把根节点下的子元素替换掉, 所以这里先保存下
    const oldEle = el.children[0]
    // 获取需要监听的元素
    const { parentRef, targetRef } = binding.value
    const items: HTMLCollectionOf<Element> = el.getElementsByClassName(targetRef)
    const target: Element = items.item(items.length - 1)
    const parent = el.getElementsByClassName(parentRef).item(0)
    // 创建实例, 将需要监听的元素传给实例
    const tip = createApp(MoreContextTip, { parent, target })
    // 目标元素挂载实例
    tip.mount(el)
    // 添加旧节点
    el.appendChild(oldEle)
  }
}

</script>
<style lang="scss" scoped>
.wrapper {
  position: relative;
}
.main {
  width: 100%;
  height: 100%;
  margin: 0 auto;
  overflow-y: auto;
  .card {
    display: inline-block;
    width: 90%;
    margin: 20px 5%;
    height: 120px;
    line-height: 120px;
    text-align: center;
    background-color: steelblue;
    font-size: 30px;
    color: #eafbff;
  }
}
</style>

指令的dom结构和样式组成的vue文件 该组件接受两个props, 一个表示监听的包裹元素, 另一个是具体监听的元素

vue
<template>
  <div ref="tip" class="v-tip">
    <div class="v-tip-text">more</div>
    <div class="v-tip-icon">
      <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-double-down" viewBox="0 0 16 16">
        <path fill-rule="evenodd" d="M1.646 6.646a.5.5 0 0 1 .708 0L8 12.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z"/>
        <path fill-rule="evenodd" d="M1.646 2.646a.5.5 0 0 1 .708 0L8 8.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z"/>
      </svg>
    </div>
  </div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from "vue";
const props = defineProps({
  parent: {
    type: Object,
    default: null,
    required: false
  },
  target: {
    type: Object,
    default: null,
    required: false
  }
})

// 如果不用ref获取元素的话, 首次加载会取不到tip元素
const tip = ref()

onMounted(() => {
  const tipEle = tip.value
  const ob = new IntersectionObserver(function(entries) {
    if (entries[0].intersectionRatio < 0.8) {
      tipEle.classList.contains('v-tip-hidden') && tipEle.classList.remove('v-tip-hidden')
    } else if (entries[0].intersectionRatio === 1) {
      !tipEle.classList.contains('v-tip-hidden') && tipEle.classList.add('v-tip-hidden')
    }
  }, {
    root: props.parent,
    threshold: [0.8, 1]
  })
  ob.observe(props.target)
})
</script>
<style lang="scss" scoped>
.v-tip {
  opacity: 1;
  transition: opacity 0.5s linear;
  .v-tip-text, .v-tip-icon {
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    font-weight: bold;
    color: #4E5969;
  }
  .v-tip-text {
    bottom: 40px;
  }
  .v-tip-icon {
    bottom: 15px;
    animation: v-tip-bounce 2s linear 0s infinite;
  }
}
.v-tip-hidden {
  opacity: 0;
}
@keyframes v-tip-bounce {
  0%,50% {
    bottom: 15px;
  }
  70% {
    bottom: 20px;
  }
  85% {
    bottom: 15px;
  }
  93% {
    bottom: 18px;
  }
  100% {
    bottom: 15px;
  }
}
</style>

4. 具体实现三(新建组件) ​

...