自定义marquee组件实现无缝滚动&鼠标悬停效果

本文介绍了Vue中的Marquee组件实现动态文本滚动的思路,通过计算内容宽度与容器宽度的关系,实现了文字在容器内的滚动,并在鼠标悬停时控制滚动。关键参数包括文字长度、起始位置、步长和间隔,以及如何处理文字溢出的情况。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

演示

在这里插入图片描述

思路

在这里插入图片描述

代码

<!--  -->
<template>
  <div class="root">
    <div style="width: 100px; height: 50px" ref="marquee">
      <marquee
        :interval="100"
        :step="4"
        :endWidth="20"
        ref="test"
        @mouseenter="stopScroll"
        @mouseleave="startScroll"
      >
        <div style="display: flex">
          <div class="item">
            哈
            <img
              src="../assets/logo.png"
              alt=""
              srcset=""
              style="width: 20px; display: inline-block; vertical-align: middle"
            />
          </div>

          <div class="item">呵呵呵呵</div>
        </div>
      </marquee>
    </div>
  </div>
</template>

<script>
import Marquee from "../components/marqueeCpn.vue";

export default {
  components: {
    Marquee,
  },
  data() {},
  //生命周期 - 创建完成(访问当前this实例)
  created() {},
  //生命周期 - 挂载完成(访问DOM元素)
  mounted() {},
  methods: {
    startScroll() {
      let marqueeRef = this.$refs.test;
      marqueeRef.run();
    },
    stopScroll() {
      let marqueeRef = this.$refs.test;
      clearInterval(marqueeRef.mysetInterval);
    },
  },
};
</script>
<style>
.root {
  margin: 0 auto;
  width: 200px;
  height: 60px;
  margin: 50px;
}
.item {
  margin-right: 10px;
}
</style>
<!--  -->
<template >
  <div class="container" ref="containerRef" :style="containerStyle">
    <div class="content" ref="contentRef" :style="contentStyle">
      <slot></slot>
    </div>
    <div
      v-if="isOverflow"
      ref="contentcpRef"
      class="contentcp"
      :style="contentcpStyle"
    >
      <slot></slot>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    // 初始位置距离左边多少px
    startWidth: {
      type: Number,
      default: 20,
    },
    // 距离右边多少距离开始循环下一跳
    endWidth: {
      type: Number,
      default: 9,
    },
    // 每interval秒滚动多少px
    step: {
      type: Number,
      default: 5,
    },
    // 每多少毫秒滚动一次
    interval: {
      type: Number,
      default: 100,
    },
  },

  data() {
    return {
      // 字的长度是否超出父盒子
      isOverflow: false,
      // 偏移量
      offset: 0,
      // 拷贝的那一份的文本的偏移量
      cpOffset: 0,
      // 垂直居中要用到
      containerHeight: "",
      // 盒子宽度
      containerWidth: 0,
      // 内容宽度
      contentWidth: 0,
      mysetInterval: null,
    };
  },
  mounted() {
    console.log("mounted");
    this.$nextTick(() => {
      const containerRef = this.$refs.containerRef;
      const contentRef = this.$refs.contentRef;

      this.containerHeight = containerRef.clientHeight;
      this.containerWidth = containerRef.clientWidth;
      this.contentWidth = contentRef.clientWidth;

      if (this.containerWidth < this.contentWidth) {
        this.isOverflow = true;
        this.offset = -(this.containerWidth - this.startWidth);
        this.run();
      } else {
        this.isOverflow = false;
      }
    });
  },
  computed: {
    containerStyle() {
      return {};
    },
    // 初始化布局
    contentStyle() {
      if (!this.isOverflow) {
        return {
          textAlign: "center",
          lineHeight: this.containerHeight + "px",
        };
      } else {
        return {
          transform: `translate3d(${this.offset}px,0,0)`,
          left: this.containerWidth + "px",
          lineHeight: this.containerHeight + "px",
        };
      }
    },
    contentcpStyle() {
      if (!this.isOverflow) {
        return;
      }
      return {
        left: this.containerWidth + "px",
        top: -this.containerHeight + "px",
        lineHeight: this.containerHeight + "px",
        transform: `translate3d(${this.cpOffset}px,0,0)`,
      };
    },
    // 第二个能进入container需要移动的距离
    openCpBox() {
      if (!this.isOverflow) {
        return;
      }
      return this.contentWidth + this.endWidth;
    },
    // 第二个能动的判断
    isOpenCpbox() {
      if (!this.isOverflow) {
        return;
      }
      // 第一个移动的距离大于openCpBox
      return -this.offset > this.openCpBox;
    },

    // 根据第二个box移动的距离判断时候第一轮已经循环完
    isToEnd() {
      if (this.containerWidth - this.startWidth + this.cpOffset < this.step) {
        return true;
      }
      return false;
    },

    startScroll() {
      // 疑问 为什么只会打印一次呢    因此这个功能移到外面去了
      console.log("mouseout");
    },

    stopScroll() {
      console.log("momuseover");
    },
  },
  methods: {
    run() {
      console.log("run");
      this.mysetInterval = setInterval(() => {
        // 第一个移动
        this.offset = this.offset - this.step;

        // 达到了第二个能移动的条件
        if (this.isOpenCpbox) {
          this.cpOffset = this.cpOffset - this.step;
        }

        // 如果第二个到达了末尾  再次初始化offset以及cpOffset的值
        if (this.isToEnd) {
          this.offset = -(this.containerWidth - this.startWidth);
          this.cpOffset = 0;
        }
      }, this.interval);
    },
  },
  //生命周期 - 创建完成(访问当前this实例)
  created() {},
  //生命周期 - 挂载完成(访问DOM元素)
  unmounted() {
    clearInterval(this.mysetInterval);
  },
};
</script>
<style>
.container {
  height: 100%;
  border: 1px solid #333;
  text-align: center;
  overflow: hidden;
}
.content {
  position: relative;

  white-space: nowrap;
  display: inline-block;
}
.contentcp {
  position: relative;
  white-space: nowrap;
  display: inline-block;
}
</style>

---- 旧版本 不要看 ----

演示

上面稍微做了些修改 下面的有些地方写的不太好(虽然修改后的也不好) 上面新版的增加了slot, 悬停效果, 所以还是看上面的代码吧 下面的就不要看了
在这里插入图片描述

在这里插入图片描述

几个参数说明

在这里插入图片描述
word:父组件传入的文字
startWidth: 这一段距离 单位px
在这里插入图片描述
endWidth:这一段距离 单位px
在这里插入图片描述

step,interval : 每interval毫秒移动多少px(step)

思路

在这里插入图片描述
container 的宽度和高度由父组件的容器决定
content就是包裹文字的容器,
在这里插入图片描述

拿到content宽度以后,需要判断是不是超过container的宽度, 赋值给isOverflow;
拿到container高度是为了让文字的line-height=container高度 来让他垂直居中
初始化布局
在这里插入图片描述
在这里插入图片描述

接下来看滚动的逻辑
第二个什么时候可以进入到container呢
这个是根据offset来计算的
在这里插入图片描述
run方法
在这里插入图片描述
第二个移动多少重新进行一次循环呢
在这里插入图片描述
在这里插入图片描述
移动到这里 ,重新进行一次循环,刚好初始化第一个的offset为-(this.containerWidth - this.startWidth);
这样的话到达start这里以后,看着是第二个在移动,其实是已经循环完了,是第一个在移动

在这里插入图片描述

代码

父组件

<!--  -->
<template>
  <div class="root">
      <marquee :word=word3  :interval="100" :step="5" :endWidth="20" style="margin:0 auto"></marquee>
  </div>
</template>

<script>
import Marquee from '../components/marqueeCpn.vue';

export default {
  components:{
    Marquee
  },
  data() {
    return {
      word1:'short',
      word2:'longlonglong我是一段很长的文字很长的文字很长的文字',
      word3:'稍微长一点的文字文字文字文字文字'
    };
  },
  //生命周期 - 创建完成(访问当前this实例)
  created() {},
  //生命周期 - 挂载完成(访问DOM元素)
  mounted() {},
};
</script>
<style>
.root{
  margin: 0 auto;
  width: 200px;
  height: 60px;
  margin: 50px;
}
</style>

marquee组件

<!--  -->
<template>
  <div class="container" :style="containerStyle">
    <div class="content" :style="contentStyle">{{ word }}</div>
    <div v-if="isOverflow" class="contentcp" :style="contentcpStyle">
      {{ word }}
    </div>
  </div>
</template>

<script>
let interval=null
export default {
  props: {
    word: String,
    // 开始滚动时距离左边间距
    startWidth: {
      type: Number,
      default: 20,
    },
    // 距离右边多少距离开始循环下一跳
    endWidth: {
      type: Number,
      default: 100,
    },
    // 每interval秒滚动多少px
    step: {
      type: Number,
      default: 5,
    },
    // 每多少毫秒滚动一次
    interval: {
      type: Number,
      default: 100,
    },
  },
  
  data() {
    return {
      // 字的长度是否超出父盒子
      isOverflow: false,
      // 偏移量
      offset: 0,
      // 拷贝的那一份的文本的偏移量
      cpOffset: 0,
      // 垂直居中要用到
      containerHeight: "",
      // 盒子宽度
      containerWidth: 0,
      // 内容宽度
      contentWidth: 0,
    };
  },
  mounted() {
    this.$nextTick(() => {
      // console.log(document.getElementsByClassName("container"));
      const containerWidth =
        document.getElementsByClassName("container")[0].clientWidth;
      const contentWidth =
        document.getElementsByClassName("content")[0].clientWidth;
      const containerHeight =
        document.getElementsByClassName("container")[0].clientHeight;
      if (containerWidth >= contentWidth) {
        this.isOverflow = false;
      } else {
        this.isOverflow = true;
      }
      // console.log(containerHeight);
      this.containerHeight = containerHeight;
      this.containerWidth = containerWidth;
      this.contentWidth = contentWidth;
      if (this.containerWidth < this.contentWidth) {
        this.isOverflow = true;
        const startWidth = this.startWidth;
        this.offset = -(this.containerWidth - startWidth);
        this.run();
      } else {
        this.isOverflow = false;
      }
    });
  },
  computed: {
    containerStyle() {
      return {};
    },
    // 初始化布局
    contentStyle() {
      if (!this.isOverflow) {
        return {
          textAlign: "center",
          lineHeight: this.containerHeight + "px",
        };
      } else {
        // console.log(`translate3d(${this.offset}px,0,0)`);
        return {
          transform: `translate3d(${this.offset}px,0,0)`,
          left: this.containerWidth + "px",
          lineHeight: this.containerHeight + "px",
        };
      }
    },
    contentcpStyle() {
      if (!this.isOverflow) {
        return;
      }
      return {
        left: this.containerWidth + "px",
        top: -this.containerHeight + "px",
        lineHeight: this.containerHeight + "px",
        transform: `translate3d(${this.cpOffset}px,0,0)`,
      };
    },
    // 第二个能进入container需要移动的距离
    openCpBox() {
      if (!this.isOverflow) {
        return;
      }
      return this.contentWidth + this.endWidth;
    },
    // 第二个能动的判断
    isOpenCpbox() {
      if (!this.isOverflow) {
        return;
      }
      return this.offset <= -this.openCpBox;
    },
    // 走完一轮的路程
    // totalDistance() {
    //   if (!this.isOverflow) {
    //     return 0;
    //   }
    //   if (this.cpOffset!=0) {
    //     // console.log(-this.cpOffset - (this.contentWidth - this.startWidth));
    //     return (
    //       -this.cpOffset - (this.contentWidth - this.startWidth) < this.step
    //     );
    //   }
    // },

    // contentRightToContainerRight() {
    //   // 第二个开始进入container逻辑
    //   if (Math.abs(this.offset - this.openCpBox) <= this.step) {
    //     console.log("第二个开始进入container逻辑");
    //     return true;
    //   }
    //   return false;
    // },
    // 根据第二个文字移动的距离判断时候第一轮已经循环完
    isToEnd() {
      if (this.containerWidth-this.startWidth+this.cpOffset<this.step) {
        return true;
      }
      return false;
    },
  },
  methods: {
    run() {
     interval = setInterval(() => {
        this.offset = this.offset - this.step;
        let offsetVal = this.offset;
        // 达到了第二个能动的条件
        if (this.isOpenCpbox) {
          this.cpOffset = this.cpOffset - this.step;
        }
        // console.log(this.cpOffset, "   ", this.totalDistance);
        // 如果第二个到达了末尾 那么重新开定时器
        if (this.isToEnd) {
          clearInterval(interval);
          this.offset = -(this.containerWidth - this.startWidth);
          this.cpOffset = 0;
          this.run();
        }
      }, this.interval);
    },
  },
  //生命周期 - 创建完成(访问当前this实例)
  created() {},
  //生命周期 - 挂载完成(访问DOM元素)
  unmounted() {
    clearInterval(interval)
  },
};
</script>
<style>
.container {
  height: 100%;
  border: 1px solid #333;
  text-align: center;
  overflow: hidden; 
}
.content {
  position: relative;

  white-space: nowrap;
  display: inline-block;
}
.contentcp {
  position: relative;
  white-space: nowrap;
  display: inline-block;
}
</style>

有个疑问

在同一个页面里面,如何使用两次这个组件,因为mounted只运行了一次,所以只拿了一次元素高度和宽度
有空把mounted里面的代码改成用watch 还有computed写 看看能不能把行
https://www.cnblogs.com/yanggb/p/12431367.html
现在暂时是建两个实例
在这里插入图片描述
但是注意有个地方的代码需要改
interval不能设成全局的了,不然会相互影响
在data里面定义
在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值