样式穿透和实现固钉效果

最近在写代码时遇到一个问题,就是我想要实现一个固钉效果,让某个元素在超过屏幕时会悬浮在屏幕上的某个位置。这就类似与flex布局。通常情况下许多的ui组件库都有提供affix组件来实现固钉效果,就比如element plus给我们提供的el-affix组件就有这样的效果,但是在某些情况之下el-affix就会失效。

在我们通常的vue项目中为了防止我的些的css样式冲突,我们都会在style上加上scoped

<style lang="scss" scoped>
<style>

关于scoped

就像这样,这时可能就会出现el-affix组件失效,其原因就是因为我加上了scoped这个属性,因为vu是单页面应用,所以我们写的不同页面最后都会被打包成一个..html文件,所以你在不同页面定义的css样式最后都会被引入到这个html文件中,这就导致如果你在不同的文件中定义的相同的文件名称,就很有可能导致样式错乱。所以就有了scoped,他的原理就是通过给样式加上识别标签来进行分类,不同的页面识别标签也就不同,
dd

如图div后面的一大串就是vue为我们加上的识别标签。
请添加图片描述

同样在引入的css文件中vue也会给我们加上识别标签,这样就不会出现样式名冲突的问题,但是我们有时在使用第三方样式库(比如element,ant等)时就会出现无法修改样式的问题,这是因为vue没有将识别标签加到组件上
请添加图片描述

vue只会在你的最外层上加上data-v-xxxxxx,但是在css文件中vue会帮你吧data-v-xxxxxx拼接在样式的最后一段。所以就导致了你都样式永远无法命中我们想要的元素,除非我们把scoped属性去掉,但是这样就可能倒是我们上面说的问题,所以我们不推荐。

样式穿透

这时我们就引入了另一个概念,就是样式穿透。样式穿透的使用方式很多,有>>>::v-deep/deep/等。在我们使用了样式穿透后vue就会把唯一的识别标志移到我们写::v-deep之后

.home_main {
  max-width: 1100px;
  margin: 48px auto 28px;
  display: flex;
  .page{
    &:deep(button){
      border-radius: 4px;
      background-color: #fff;
      box-shadow: 0 2px 1px -2px rgb(0 0 0 / 20%), 0 2px 2px 0 rgb(0 0 0 / 14%), 0 1px 5px 0 rgb(0 0 0 / 12%);
    }
    &:deep(.el-pager){
     .number{
       border-radius: 4px;
       background-color: #fff;
       box-shadow: 0 2px 1px -2px rgb(0 0 0 / 20%), 0 2px 2px 0 rgb(0 0 0 / 14%), 0 1px 5px 0 rgb(0 0 0 / 12%);
     }
     .more{
       border-radius: 4px;
       box-shadow: 0 2px 1px -2px rgb(0 0 0 / 20%), 0 2px 2px 0 rgb(0 0 0 / 14%), 0 1px 5px 0 rgb(0 0 0 / 12%);
       background-color: #fff;
     }
     .active{
       box-shadow: 0 2px 4px -1px rgb(0 0 0 / 20%), 0 4px 5px 0 rgb(0 0 0 / 14%), 0 1px 10px 0 rgb(0 0 0 / 12%);
     }
    }
  }

在这里插入图片描述

再看没有加样式穿透之前的样式
在这里插入图片描述

随意通过样式穿透我们就可以实现scoped所带来的副作用,顺便一提>>>的兼容性不是很高,所以不是很推荐,最好写后面两种。(因为最近在写vue3的项目,在vue3中提示使用&:deep(.name)来实现样式穿透,目前我也不知道他是vue3的特定写法还是,新的写法,就和大家分享一下。还有一个我觉得说样式穿透讲的挺好的视频https://www.douyin.com/video/6987146287137180966 如果还是有不懂什么是样式穿透以及原理的,推荐可以看看)

.el-card {
  &:deep(.el-card__body) {
    padding: 0;
    display: flex;
    height: 250px;
    align-items: center;
  }
}

说到这里大部分的样式问题都可以解决,但是当我去尝试el-effix时结果发现还是不行,弄得我快疯了,最后去看了el-affix的源码才知道,elememt plus 是通过判断el-affix指定的容器,当超过屏幕时给内层div添加样式,让el-affix变成悬浮的状态,在没超过屏幕的状态下移除div的悬浮样式
AAA
在这里插入图片描述

样式穿透可以解决当容器超出屏幕悬浮的问题,但是当回到原始位置时,就无法消除内层div的el-affix--fixed样式,导致出现一直悬浮的状态,可能是我技术还不够吧,如果有解决的朋友可以分享下。

我只好另找出路,既然element plus的组件用不了,那就自己写一个(其实是网上cv的),由于我的项目的vue3+ts的,但是网上大多都是vue2和js的所以就自己改了那么一点点,本人小白,写的不好的地方还请轻点喷,不多说,上代码

<template>
  <div class="affix-placeholder" :style="data.wrapStyle">
    <div class="affix-div" :class="{'affix': data.affixed}" :style="data.styles">
      <slot />
    </div>
  </div>
</template>

<script lang="ts">
import {
  computed,
  defineComponent,
  getCurrentInstance,
  onMounted,
  reactive,
  onUnmounted } from 'vue';
import { Styles, affixProps } from './affixProps';

export default defineComponent({
  name: 'Affix',
  props: affixProps,

  setup(props: any) {
    const data = reactive({
      affixed: false,
      styles: {} as Styles,
      affixedClientHeight: 0,
      wrapStyle: {},
    });
    const instance = getCurrentInstance();
    const getScroll = function(w: Window, top?: boolean){
      let ret = w[`page${(top ? 'Y' : 'X')}Offset`];
      const method = `scroll${top ? 'Top' : 'Left'}`;
      if (typeof ret !== 'number') {
        const d = w.document;
        // ie6,7,8 standard mode
        ret = d.documentElement[method];
        if (typeof ret !== 'number') {
          // quirks mode
          ret = d.body[method];
        }
      }
      return ret;
    };
    const getOffset = function (element: Element) {
      const rect = element.getBoundingClientRect();
      const body = document.body;
      const clientTop = element.clientTop || body.clientTop || 0;
      const clientLeft = element.clientLeft || body.clientLeft || 0;
      const scrollTop = getScroll(window, true);
      const scrollLeft = getScroll(window);
      return {
        top: rect.bottom + scrollTop - clientTop - data.affixedClientHeight,
        left: rect.left + scrollLeft - clientLeft
      };
    };
    const offsets = computed(()=>{
      if (props.boundary) {
        return 0;
      }
      return props.offset;
    });
    const handleScroll = () => {
      const scrollTop = getScroll(window, true) + offsets.value; // handle setting offset
      const elementOffset = getOffset(instance?.proxy?.$el);
      if (!data.affixed && scrollTop > elementOffset.top) {
        data.affixed = true;
        data.styles = {
          top: `${offsets.value}px`,
          left: `${elementOffset.left}px`,
          width: `${instance?.proxy?.$el.offsetWidth}px`
        };
        props.onAffix(data.affixed);
      }
      // if setting boundary
      if (props.boundary && scrollTop > elementOffset.top) {
        const el = document.getElementById(props.boundary);
        if (el) {
          const boundaryOffset = getOffset(el);
          if ((scrollTop + offsets.value) > boundaryOffset.top) {
            const top = scrollTop - boundaryOffset.top;
            data.styles.top = `-${top}px`;
          }
        }
      }
      if (data.affixed && scrollTop < elementOffset.top) {
        data.affixed = false;
        data.styles = {};
        props.onAffix(data.affixed);
      }
      if (data.affixed && props.boundary) {
        const el = document.getElementById(props.boundary.slice(1));
        if (el) {
          const boundaryOffset = getOffset(el);
          if ((scrollTop + offsets.value) <= boundaryOffset.top) {
            data.styles.top = 0;
          }
        }
      }
    };
    onMounted(()=> {
      data.affixedClientHeight = instance?.proxy?.$el.children[0].clientHeight;
      data.wrapStyle = { height: `${data.affixedClientHeight}px` };
      window.addEventListener('scroll', handleScroll);
      window.addEventListener('resize', handleScroll);
    });
    onUnmounted(()=>{
      window.removeEventListener('scroll', handleScroll);
      window.removeEventListener('resize', handleScroll);
    });
    return {
      data,
    };
  }
});
</script>

<style lang="sass">
.affix
  position: fixed
</style>

import { PropType } from 'vue';
import { FunctionType } from '@/constant/Type';


export interface Styles{
  top?: string | number,
  left?: string | number,
  width?: string | number,
}

export const affixProps = {
  offset: {
    type: Number as PropType<number>,
    default: 0,
  },
  onAffix: {
    type: Function as PropType<FunctionType>,
    default() {}
  },
  boundary: {
    type: String as PropType<string>,
    default: 0,
  }
};

export interface FunctionType {
  (): void;
}

这样一个affix组件就写完了,但是经过我的一轮测试,发现还是不行,最后发现是因为自定义的affix组件是在初始化时获取父级组件或绑定的父级组件的高度,但是我的父级组件的高度是动态的,通过后端获取数据后再通过v-for渲染,所以初始高度很小,在高度改变后的affix内的父级高度却没有发生变化。

sticky粘性布局

在经过了又一番的查找资料后,我又看到了一个解决方案,就是posistion:sticky在css的posistion中我们常用到的属性有relativeabsolute,sticky就在粘性布局我们可以通过设置

position: -webkit-sticky;
position: sticky;

这个属性来实现类似固钉的效果,在官方文档中的说明是sticky 英文字面意思是粘,粘贴,所以可以把它称之为粘性定位。

position: sticky; 基于用户的滚动位置来定位。

粘性定位的元素是依赖于用户的滚动,在 position:relative 与 position:fixed 定位之间切换。

它的行为就像 position:relative; 而当页面滚动超出目标区域时,它的表现就像 position:fixed;,它会固定在目标位置。

元素定位表现为在跨越特定阈值前为相对定位,之后为固定定位。

这个特定阈值指的是 top, right, bottom 或 left 之一,换言之,指定 top, right, bottom 或 left 四个阈值其中之一,才可使粘性定位生效。否则其行为与相对定位相同。

反正看这么多也不是很懂先cv再说,不出意外的话就要出意外了,果然不行,没办法只能再去看文档了,最后才知道posistion:stucky要生效也需要有条件,总结来说

  1. 须指定 top, right, bottom 或 left 四个阈值其中之一,就是需要设置这几个元素任意一个值,并且 top 和 bottom 同时设置时,top 生效的优先级高,left 和 right 同时设置时,left 的优先级高。
  2. 最重要的一点,在设置了posistion:stucky后该元素的所有父级元素都不能设置overflow:hidden不然也不会生效,具体可以查看官方文档。

最后终于可以了,就这一个问题困了我三天,本以为事情就这样结束了,结果过几天天我在加其他的功能时突然发现,又失灵了。。。。。

最后排查了好久才发现原来是原先在设置el-scrollbaroverflow时没有设置成功,再加一个

overflow: visible !important;

终于所有问题都解决了。

Logo

前往低代码交流专区

更多推荐