需求效果图

我们先看看对应的需求效果图

完整代码在文末,笔者的github仓库中

给el-table吸顶

给一些dom元素吸顶

实现过程

思路分析

  • 因为css中的sticky兼容性不是特别好
  • 所以我们使用fixed搭配滚动,去动态控制dom元素是否固定定位
  • 首先,吸顶效果一般是整页滚动条 【就是以html或body作为根元素滚动条】
  • 因为使用的是Vue的框架,所以我们把html和body都设置成定高禁止滚动
  • 把滚动的元素更改为#app 如下:
html,
body {
  margin: 0;
  width: 100vw;
  height: 100vh;
  background-color: #fff;
}

#app {
  color: #363636;
  overflow-y: auto;
}
  • 所以我们就可以给根元素#app绑定滚动事件,并在滚动事件的句柄中,去看看 用于计算距离的dom元素 距离页面视口顶部距离是多少
  • 当距离小于等于0的时候,我们就给 用于吸顶固定定位的元素 加上固定定位的相关样式
  • 这里要提一下,三个关键词:

1. 用于计算距离的dom元素

2. 用于吸顶固定定位的元素

3. 固定定位的相关样式

图示:用于计算距离的dom元素、用于吸顶固定定位的元素、和样式

  • 在#app容器中滚动,我们需要首先看看距离顶部视口的距离,再给吸顶元素添加固定定位的样式
  • 如下图:

  • 至于固定定位的相关样式,则是可以通过传递一个类名去控制
  • 因为有时候,我们除了添加 position: fixed; 之外,还要去添加其他的样式(如本文第二张图,吸顶的时候,要给吸顶元素添加一个阴影和底部的边框)

再加一个层级高度控制,注意吸顶元素的遮挡问题

  • 某些情况下,我们需要给吸顶的元素抬高层级
  • 否则会挡住一些组件,所以还需要再指定一个层级(有默认值)不让其挡住一些组件

如下效果图:

代码步骤

引入、使用、注册

主入口文件引入:

// main.js
import install from './directives' // 引入并使用自定义指令
app.use(install)

directives文件夹

// directives文件夹下的index暴露的出口文件
// 引入各个自定义指令
import stick from "./stick/index";

// 自定义指令对象,用于遍历注册
const directives = {
    stick,
}
// 批量注册指令并暴露到main.js中去便于注册use
export default {
    install(Vue) {
        Object.keys(directives).forEach((key) => {
            Vue.directive(key, directives[key])
        })
    }
}

编写自定义指令v-stick代码

directives/stick/index.js

// 单独拎出来一个css文件,用于维护吸顶元素的样式
import './index.css'

// 计算距离顶部高度的元素,如el-table就是表格最外层容器元素以class为例
let calcDom = null
// 需要吸顶的元素,如el-table需要吸顶的就是表头元素
let stickDom = null
// 指定吸顶固定定位的样式类名
let className = ''
// 指定默认层级,防止层级太高遮挡住别的层
let zIndex = 1000

export default {
    mounted(el, binding) {
        const { calcDomClass, stickDomClass, fixedName, zInd } = binding.value
        if (!calcDomClass || !stickDomClass || !fixedName) throw Error('need >= 3 params')
        calcDom = document.querySelector(calcDomClass)
        stickDom = document.querySelector(stickDomClass)
        className = fixedName
        zInd ? zIndex = zInd : null
        setFixed()
    },
    unmounted(el, binding) {
        removeFixed()
    }
}

// 绑定监听滚动事件
const setFixed = () => {
    // 这里以挂载元素#app为滚动容器,body或html也是一个意思
    const scrollWrap = document.querySelector('#app')
    scrollWrap?.addEventListener('scroll', handle)
}

// 解绑监听滚动事件
const removeFixed = () => {
    // 这里以挂载元素#app为滚动容器,body或html也是一个意思
    const scrollWrap = document.querySelector('#app')
    scrollWrap?.removeEventListener('scroll', handle)
    calcDom = null
    stickDom = null
    className = ''
    zIndex = 1000
}

const handle = (e) => {
    console.log(147)
    let topDistance = calcDom?.getBoundingClientRect().top || 0
    // 距离小于等于0,不在可视区域内时,添加固定定位吸顶
    if (topDistance <= 0) {
        add(stickDom)
    }
    // 距离大于0,在可视区域内时,取消固定定位吸顶
    else {
        remove(stickDom)
    }
}

const add = (stickDom) => {
    if (stickDom) {
        if (stickDom.style.position != 'fixed') {
            stickDom.classList.add(className)
            stickDom.style.zIndex = zIndex // 层级单独处理
        }
    }
}

const remove = (stickDom) => {
    if (stickDom) {
        if (stickDom.classList.contains(className)) {
            stickDom.classList.remove(className)
            stickDom.style.zIndex = 'initial' // 层级单独处理
        }
    }
}

directives/stick/index.css

/* 固定el-table添加的样式 */
.fixedElTable {
    position: fixed;
    top: 0;
}

/* 固定H1元素添加的样式 */
.fixedH1Dom {
    position: fixed;
    top: 0;
    box-shadow: rgba(113, 100, 100, 0.42) 0px 6px 20px;
    border-bottom: 3px solid #79BBFF;
    box-sizing: border-box;
}

el-table使用示例

<template>
    <h2 v-for="i in 10" :key="i">吸顶效果</h2>
    <el-table v-stick="{
        calcDomClass: '.el-table',
        stickDomClass: '.el-table__header-wrapper',
        fixedName: 'fixedElTable',
    }" :data="tableData" border style="width: 100%" :header-cell-style="{ background: '#999', color: '#000' }">
        <el-table-column prop="name" label="姓名" width="180" />
        <el-table-column prop="age" label="年龄" width="180" />
        <el-table-column prop="home" label="家乡" />
    </el-table>
</template>
  
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount } from "vue";

interface row {
    name: String,
    age: Number,
    home: String
}
const tableData = ref([] as row[])

const initData = () => {
    for (let i = 0; i < 50; i++) {
        tableData.value.push({
            name: '孙悟空' + i,
            age: 500 + i,
            home: '花果山' + i
        })
    }
}
initData()
</script>

普通dom元素使用示例

<template>
    <h2 v-for="i in 10" :key="i">吸顶效果{{ i }}</h2>
    <div class="useForCalc">
        <h1 class="useForStick" v-stick="{
            calcDomClass: '.useForCalc',
            stickDomClass: '.useForStick',
            fixedName: 'fixedH1Dom',
            zInd: 10000
        }">--------吸顶元素-------- <button @click="showLevel">吸顶时点击</button> </h1>
    </div>
    <h2 v-for="i in 40" :key="i">吸顶效果{{ i }}</h2>
</template>

<script lang="ts" setup>
import { ElMessage } from 'element-plus'

const showLevel = () => {
    ElMessage({
        message: 'Congrats, this is a success message.',
        type: 'success',
        offset: 28
    })
}

</script>

<style>
h1 {
    width: 480px;
    background-color: #999;
    display: flex;
}
button {
    padding: 6px 12px;
}
</style>

仓库代码地址

完整示例,在笔者的github仓库代码里面(仓库后续会更新一些功能,欢迎不吝star)

https://github.com/shuirongshuifu/vue3-echarts5-example

可以自行加上lodash的节流做滚动,当然加不加取决于滚动吸顶效果是否炒鸡丝滑

水冗水孚
1.1k 声望585 粉丝

每一个不曾起舞的日子,都是对生命的辜负